/* This file is part of the KDE libraries Copyright (C) 2008, 2009 Andreas Hartmetz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "httpauthentication.h" #ifdef HAVE_LIBGSSAPI #ifdef GSSAPI_MIT #include #else #include #endif /* GSSAPI_MIT */ // Catch uncompatible crap (BR86019) #if defined(GSS_RFC_COMPLIANT_OIDS) && (GSS_RFC_COMPLIANT_OIDS == 0) #include #define GSS_C_NT_HOSTBASED_SERVICE gss_nt_service_name #endif #endif /* HAVE_LIBGSSAPI */ #include #include #include #include #include #include #include #include #include static bool isWhiteSpace(char ch) { return (ch == ' ' || ch == '\t' || ch == '\v' || ch == '\f'); } static bool isWhiteSpaceOrComma(char ch) { return (ch == ',' || isWhiteSpace(ch)); } static bool containsScheme(const char input[], int start, int end) { // skip any comma or white space while (start < end && isWhiteSpaceOrComma(input[start])) { start++; } while (start < end) { if (isWhiteSpace(input[start])) { return true; } start++; } return false; } // keys on even indexes, values on odd indexes. Reduces code expansion for the templated // alternatives. // If "ba" starts with empty content it will be removed from ba to simplify later calls static QList parseChallenge(QByteArray &ba, QByteArray *scheme, QByteArray* nextAuth = 0) { QList values; const char *b = ba.constData(); int len = ba.count(); int start = 0, end = 0, pos = 0, pos2 = 0; // parse scheme while (start < len && isWhiteSpaceOrComma(b[start])) { start++; } end = start; while (end < len && !isWhiteSpace(b[end])) { end++; } // drop empty stuff from the given string, it would have to be skipped over and over again if (start != 0) { ba = ba.mid(start); end -= start; len -= start; start = 0; b = ba.constData(); } Q_ASSERT(scheme); *scheme = ba.left(end); while (end < len) { start = end; while (end < len && b[end] != '=') { end++; } pos = end; // save the end position while (end - 1 > start && isWhiteSpace(b[end - 1])) { // trim whitespace end--; } pos2 = start; while (pos2 < end && isWhiteSpace(b[pos2])) { // skip whitespace pos2++; } if (containsScheme(b, start, end) || (b[pos2] == ',' && b[pos] != '=' && pos == len)) { if (nextAuth) { *nextAuth = QByteArray (b + start); } break; // break on start of next scheme. } while (start < len && isWhiteSpaceOrComma(b[start])) { start++; } values.append(QByteArray (b + start, end - start)); end = pos; // restore the end position if (end == len) { break; } // parse value start = end + 1; //skip '=' while (start < len && isWhiteSpace(b[start])) { start++; } if (b[start] == '"') { //quoted string bool hasBs = false; bool hasErr = false; end = ++start; while (end < len) { if (b[end] == '\\') { end++; if (end + 1 >= len) { hasErr = true; break; } else { hasBs = true; end++; } } else if (b[end] == '"') { break; } else { end++; } } if (hasErr || (end == len)) { // remove the key we already inserted kDebug(7113) << "error in quoted text for key" << values.last(); values.removeLast(); break; } QByteArray value = QByteArray(b + start, end - start); if (hasBs) { // skip over the next character, it might be an escaped backslash int i = -1; while ( (i = value.indexOf('\\', i + 1)) >= 0 ) { value.remove(i, 1); } } values.append(value); end++; } else { //unquoted string end = start; while (end < len && b[end] != ',' && !isWhiteSpace(b[end])) { end++; } values.append(QByteArray(b + start, end - start)); } //the quoted string has ended, but only a comma ends a key-value pair while (end < len && isWhiteSpace(b[end])) { end++; } // garbage, here should be end or field delimiter (comma) if (end < len && b[end] != ',') { kDebug(7113) << "unexpected character" << b[end] << "found in WWW-authentication header where token boundary (,) was expected"; break; } } // ensure every key has a value // WARNING: Do not remove the > 1 check or parsing a Type 1 NTLM // authentication challenge will surely fail. if (values.count() > 1 && values.count() % 2) { values.removeLast(); } return values; } static QByteArray valueForKey(const QList &ba, const QByteArray &key) { for (int i = 0, count = ba.count(); (i + 1) < count; i += 2) { if (ba[i] == key) { return ba[i + 1]; } } return QByteArray(); } KAbstractHttpAuthentication::KAbstractHttpAuthentication(KConfigGroup *config) :m_config(config), m_finalAuthStage(false) { reset(); } KAbstractHttpAuthentication::~KAbstractHttpAuthentication() { } QByteArray KAbstractHttpAuthentication::bestOffer(const QList &offers) { // choose the most secure auth scheme offered QByteArray negotiateOffer; QByteArray digestOffer; QByteArray ntlmOffer; QByteArray basicOffer; Q_FOREACH (const QByteArray &offer, offers) { const QByteArray scheme = offer.mid(0, offer.indexOf(' ')).toLower(); #ifdef HAVE_LIBGSSAPI if (scheme == "negotiate") { // krazy:exclude=strings negotiateOffer = offer; } else #endif if (scheme == "digest") { // krazy:exclude=strings digestOffer = offer; } else if (scheme == "ntlm") { // krazy:exclude=strings ntlmOffer = offer; } else if (scheme == "basic") { // krazy:exclude=strings basicOffer = offer; } } if (!negotiateOffer.isEmpty()) { return negotiateOffer; } if (!digestOffer.isEmpty()) { return digestOffer; } if (!ntlmOffer.isEmpty()) { return ntlmOffer; } return basicOffer; //empty or not... } KAbstractHttpAuthentication *KAbstractHttpAuthentication::newAuth(const QByteArray &offer, KConfigGroup* config) { const QByteArray scheme = offer.mid(0, offer.indexOf(' ')).toLower(); #ifdef HAVE_LIBGSSAPI if (scheme == "negotiate") { // krazy:exclude=strings return new KHttpNegotiateAuthentication(config); } else #endif if (scheme == "digest") { // krazy:exclude=strings return new KHttpDigestAuthentication(); } else if (scheme == "ntlm") { // krazy:exclude=strings return new KHttpNtlmAuthentication(config); } else if (scheme == "basic") { // krazy:exclude=strings return new KHttpBasicAuthentication(); } return 0; } QList< QByteArray > KAbstractHttpAuthentication::splitOffers(const QList< QByteArray >& offers) { // first detect if one entry may contain multiple offers QList alloffers; foreach(QByteArray offer, offers) { QByteArray scheme, cont; parseChallenge(offer, &scheme, &cont); while (!cont.isEmpty()) { offer.chop(cont.length()); alloffers << offer; offer = cont; cont.clear(); parseChallenge(offer, &scheme, &cont); } alloffers << offer; } return alloffers; } void KAbstractHttpAuthentication::reset() { m_scheme.clear(); m_challenge.clear(); m_challengeText.clear(); m_resource.clear(); m_httpMethod.clear(); m_isError = false; m_needCredentials = true; m_forceKeepAlive = false; m_forceDisconnect = false; m_keepPassword = false; m_headerFragment.clear(); m_username.clear(); m_password.clear(); } void KAbstractHttpAuthentication::setChallenge(const QByteArray &c, const KUrl &resource, const QByteArray &httpMethod) { reset(); m_challengeText = c.trimmed(); m_challenge = parseChallenge(m_challengeText, &m_scheme); Q_ASSERT(m_scheme.toLower() == scheme().toLower()); m_resource = resource; m_httpMethod = httpMethod; } QString KAbstractHttpAuthentication::realm() const { const QByteArray realm = valueForKey(m_challenge, "realm"); // TODO: Find out what this is supposed to address. The site mentioned below does not exist. if (KGlobal::locale()->language().contains(QLatin1String("ru"))) { //for sites like lib.homelinux.org return QTextCodec::codecForName("CP1251")->toUnicode(realm); } return QString::fromLatin1(realm.constData(), realm.length()); } void KAbstractHttpAuthentication::authInfoBoilerplate(KIO::AuthInfo *a) const { a->url = m_resource; a->username = m_username; a->password = m_password; a->verifyPath = supportsPathMatching(); a->realmValue = realm(); a->digestInfo = QLatin1String(authDataToCache()); a->keepPassword = m_keepPassword; } void KAbstractHttpAuthentication::generateResponseCommon(const QString &user, const QString &password) { if (m_scheme.isEmpty() || m_httpMethod.isEmpty()) { m_isError = true; return; } if (m_needCredentials) { m_username = user; m_password = password; } m_isError = false; m_forceKeepAlive = false; m_forceDisconnect = false; m_finalAuthStage = true; } QByteArray KHttpBasicAuthentication::scheme() const { return "Basic"; } void KHttpBasicAuthentication::fillKioAuthInfo(KIO::AuthInfo *ai) const { authInfoBoilerplate(ai); } void KHttpBasicAuthentication::generateResponse(const QString &user, const QString &password) { generateResponseCommon(user, password); if (m_isError) { return; } m_headerFragment = "Basic "; m_headerFragment += QByteArray(m_username.toLatin1() + ':' + m_password.toLatin1()).toBase64(); m_headerFragment += "\r\n"; } QByteArray KHttpDigestAuthentication::scheme() const { return "Digest"; } void KHttpDigestAuthentication::setChallenge(const QByteArray &c, const KUrl &resource, const QByteArray &httpMethod) { QString oldUsername; QString oldPassword; if (valueForKey(m_challenge, "stale").toLower() == "true") { // stale nonce: the auth failure that triggered this round of authentication is an artifact // of digest authentication. the credentials are probably still good, so keep them. oldUsername = m_username; oldPassword = m_password; } KAbstractHttpAuthentication::setChallenge(c, resource, httpMethod); if (!oldUsername.isEmpty() && !oldPassword.isEmpty()) { // keep credentials *and* don't ask for new ones m_needCredentials = false; m_username = oldUsername; m_password = oldPassword; } } void KHttpDigestAuthentication::fillKioAuthInfo(KIO::AuthInfo *ai) const { authInfoBoilerplate(ai); } struct DigestAuthInfo { QByteArray nc; QByteArray qop; QByteArray realm; QByteArray nonce; QByteArray method; QByteArray cnonce; QByteArray username; QByteArray password; KUrl::List digestURIs; QByteArray algorithm; QByteArray entityBody; }; //calculateResponse() from the original HTTPProtocol static QByteArray calculateResponse(const DigestAuthInfo &info, const KUrl &resource) { KMD5 md; QByteArray HA1; QByteArray HA2; // Calculate H(A1) QByteArray authStr = info.username; authStr += ':'; authStr += info.realm; authStr += ':'; authStr += info.password; md.update( authStr ); if ( info.algorithm.toLower() == "md5-sess" ) { authStr = md.hexDigest(); authStr += ':'; authStr += info.nonce; authStr += ':'; authStr += info.cnonce; md.reset(); md.update( authStr ); } HA1 = md.hexDigest(); kDebug(7113) << "A1 => " << HA1; // Calcualte H(A2) authStr = info.method; authStr += ':'; authStr += resource.encodedPathAndQuery(KUrl::LeaveTrailingSlash, KUrl::AvoidEmptyPath).toLatin1(); if ( info.qop == "auth-int" ) { authStr += ':'; md.reset(); md.update(info.entityBody); authStr += md.hexDigest(); } md.reset(); md.update( authStr ); HA2 = md.hexDigest(); kDebug(7113) << "A2 => " << HA2; // Calcualte the response. authStr = HA1; authStr += ':'; authStr += info.nonce; authStr += ':'; if ( !info.qop.isEmpty() ) { authStr += info.nc; authStr += ':'; authStr += info.cnonce; authStr += ':'; authStr += info.qop; authStr += ':'; } authStr += HA2; md.reset(); md.update( authStr ); const QByteArray response = md.hexDigest(); kDebug(7113) << "Response =>" << response; return response; } void KHttpDigestAuthentication::generateResponse(const QString &user, const QString &password) { generateResponseCommon(user, password); if (m_isError) { return; } // magic starts here (this part is slightly modified from the original in HTTPProtocol) DigestAuthInfo info; info.username = m_username.toLatin1(); //### charset breakage info.password = m_password.toLatin1(); //### // info.entityBody = p; // FIXME: send digest of data for POST action ?? info.realm = ""; info.nonce = ""; info.qop = ""; // cnonce is recommended to contain about 64 bits of entropy #ifdef ENABLE_HTTP_AUTH_NONCE_SETTER info.cnonce = m_nonce; #else info.cnonce = KRandom::randomString(16).toLatin1(); #endif // HACK: Should be fixed according to RFC 2617 section 3.2.2 info.nc = "00000001"; // Set the method used... info.method = m_httpMethod; // Parse the Digest response.... info.realm = valueForKey(m_challenge, "realm"); info.algorithm = valueForKey(m_challenge, "algorithm"); if (info.algorithm.isEmpty()) { info.algorithm = valueForKey(m_challenge, "algorith"); } if (info.algorithm.isEmpty()) { info.algorithm = "MD5"; } Q_FOREACH (const QByteArray &path, valueForKey(m_challenge, "domain").split(' ')) { KUrl u(m_resource, QString::fromLatin1(path)); if (u.isValid()) { info.digestURIs.append(u); } } info.nonce = valueForKey(m_challenge, "nonce"); QByteArray opaque = valueForKey(m_challenge, "opaque"); info.qop = valueForKey(m_challenge, "qop"); // NOTE: Since we do not have access to the entity body, we cannot support // the "auth-int" qop value ; so if the server returns a comma separated // list of qop values, prefer "auth".See RFC 2617 sec 3.2.2 for the details. // If "auth" is not present or it is set to "auth-int" only, then we simply // print a warning message and disregard the qop option altogether. if (info.qop.contains(',')) { const QList values = info.qop.split(','); if (info.qop.contains("auth")) info.qop = "auth"; else { kWarning(7113) << "Unsupported digest authentication qop parameters:" << values; info.qop.clear(); } } else if (info.qop == "auth-int") { kWarning(7113) << "Unsupported digest authentication qop parameter:" << info.qop; info.qop.clear(); } if (info.realm.isEmpty() || info.nonce.isEmpty()) { // ### proper error return m_isError = true; return; } // If the "domain" attribute was not specified and the current response code // is authentication needed, add the current request url to the list over which // this credential can be automatically applied. if (info.digestURIs.isEmpty() /*###&& (m_request.responseCode == 401 || m_request.responseCode == 407)*/) info.digestURIs.append (m_resource); else { // Verify whether or not we should send a cached credential to the // server based on the stored "domain" attribute... bool send = true; // Determine the path of the request url... QString requestPath = m_resource.directory(KUrl::AppendTrailingSlash | KUrl::ObeyTrailingSlash); if (requestPath.isEmpty()) requestPath = QLatin1Char('/'); Q_FOREACH (const KUrl &u, info.digestURIs) { send &= (m_resource.protocol().toLower() == u.protocol().toLower()); send &= (m_resource.host().toLower() == u.host().toLower()); if (m_resource.port() > 0 && u.port() > 0) send &= (m_resource.port() == u.port()); QString digestPath = u.directory (KUrl::AppendTrailingSlash | KUrl::ObeyTrailingSlash); if (digestPath.isEmpty()) digestPath = QLatin1Char('/'); send &= (requestPath.startsWith(digestPath)); if (send) break; } if (!send) { m_isError = true; return; } } kDebug(7113) << "RESULT OF PARSING:"; kDebug(7113) << " algorithm: " << info.algorithm; kDebug(7113) << " realm: " << info.realm; kDebug(7113) << " nonce: " << info.nonce; kDebug(7113) << " opaque: " << opaque; kDebug(7113) << " qop: " << info.qop; // Calculate the response... const QByteArray response = calculateResponse(info, m_resource); QByteArray auth = "Digest username=\""; auth += info.username; auth += "\", realm=\""; auth += info.realm; auth += "\""; auth += ", nonce=\""; auth += info.nonce; auth += "\", uri=\""; auth += m_resource.encodedPathAndQuery(KUrl::LeaveTrailingSlash, KUrl::AvoidEmptyPath).toLatin1(); if (!info.algorithm.isEmpty()) { auth += "\", algorithm="; auth += info.algorithm; } if ( !info.qop.isEmpty() ) { auth += ", qop="; auth += info.qop; auth += ", cnonce=\""; auth += info.cnonce; auth += "\", nc="; auth += info.nc; } auth += ", response=\""; auth += response; if ( !opaque.isEmpty() ) { auth += "\", opaque=\""; auth += opaque; } auth += "\"\r\n"; // magic ends here // note that auth already contains \r\n m_headerFragment = auth; } #ifdef ENABLE_HTTP_AUTH_NONCE_SETTER void KHttpDigestAuthentication::setDigestNonceValue(const QByteArray& nonce) { m_nonce = nonce; } #endif QByteArray KHttpNtlmAuthentication::scheme() const { return "NTLM"; } void KHttpNtlmAuthentication::setChallenge(const QByteArray &c, const KUrl &resource, const QByteArray &httpMethod) { QString oldUsername, oldPassword; if (!m_finalAuthStage && !m_username.isEmpty() && !m_password.isEmpty()) { oldUsername = m_username; oldPassword = m_password; } KAbstractHttpAuthentication::setChallenge(c, resource, httpMethod); if (!oldUsername.isEmpty() && !oldPassword.isEmpty()) { m_username = oldUsername; m_password = oldPassword; } // The type 1 message we're going to send needs no credentials; // they come later in the type 3 message. m_needCredentials = m_challenge.isEmpty(); } void KHttpNtlmAuthentication::fillKioAuthInfo(KIO::AuthInfo *ai) const { authInfoBoilerplate(ai); // Every auth scheme is supposed to supply a realm according to the RFCs. Of course this doesn't // prevent Microsoft from not doing it... Dummy value! // we don't have the username yet which may (may!) contain a domain, so we really have no choice ai->realmValue = QLatin1String("NTLM"); } void KHttpNtlmAuthentication::generateResponse(const QString &_user, const QString &password) { generateResponseCommon(_user, password); if (m_isError) { return; } QByteArray buf; if (m_challenge.isEmpty()) { m_finalAuthStage = false; // first, send type 1 message (with empty domain, workstation..., but it still works) if (!KNTLM::getNegotiate(buf)) { kWarning(7113) << "Error while constructing Type 1 NTLM authentication request"; m_isError = true; return; } } else { m_finalAuthStage = true; // we've (hopefully) received a valid type 2 message: send type 3 message as last step QString user, domain; if (m_username.contains(QLatin1Char('\\'))) { domain = m_username.section(QLatin1Char('\\'), 0, 0); user = m_username.section(QLatin1Char('\\'), 1); } else { user = m_username; } m_forceKeepAlive = true; const QByteArray challenge = QByteArray::fromBase64(m_challenge[0]); KNTLM::AuthFlags flags = KNTLM::Add_LM; if (!m_config || !m_config->readEntry("EnableNTLMv2Auth", false)) { flags |= KNTLM::Force_V1; } if (!KNTLM::getAuth(buf, challenge, user, m_password, domain, QLatin1String("WORKSTATION"), flags)) { kWarning(7113) << "Error while constructing Type 3 NTLM authentication request"; m_isError = true; return; } } m_headerFragment = "NTLM "; m_headerFragment += buf.toBase64(); m_headerFragment += "\r\n"; return; } ////////////////////////// #ifdef HAVE_LIBGSSAPI // just an error message formatter static QByteArray gssError(int major_status, int minor_status) { OM_uint32 new_status; OM_uint32 msg_ctx = 0; gss_buffer_desc major_string; gss_buffer_desc minor_string; OM_uint32 ret; QByteArray errorstr; do { ret = gss_display_status(&new_status, major_status, GSS_C_GSS_CODE, GSS_C_NULL_OID, &msg_ctx, &major_string); errorstr += (const char *)major_string.value; errorstr += ' '; ret = gss_display_status(&new_status, minor_status, GSS_C_MECH_CODE, GSS_C_NULL_OID, &msg_ctx, &minor_string); errorstr += (const char *)minor_string.value; errorstr += ' '; } while (!GSS_ERROR(ret) && msg_ctx != 0); return errorstr; } QByteArray KHttpNegotiateAuthentication::scheme() const { return "Negotiate"; } void KHttpNegotiateAuthentication::setChallenge(const QByteArray &c, const KUrl &resource, const QByteArray &httpMethod) { KAbstractHttpAuthentication::setChallenge(c, resource, httpMethod); // GSSAPI knows how to get the credentials on its own m_needCredentials = false; } void KHttpNegotiateAuthentication::fillKioAuthInfo(KIO::AuthInfo *ai) const { authInfoBoilerplate(ai); //### does GSSAPI supply anything realm-like? dummy value for now. ai->realmValue = QLatin1String("Negotiate"); } void KHttpNegotiateAuthentication::generateResponse(const QString &user, const QString &password) { generateResponseCommon(user, password); if (m_isError) { return; } OM_uint32 major_status, minor_status; gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; gss_name_t server; gss_ctx_id_t ctx; gss_OID mech_oid; static gss_OID_desc krb5_oid_desc = {9, (void *) "\x2a\x86\x48\x86\xf7\x12\x01\x02\x02"}; static gss_OID_desc spnego_oid_desc = {6, (void *) "\x2b\x06\x01\x05\x05\x02"}; gss_OID_set mech_set; gss_OID tmp_oid; ctx = GSS_C_NO_CONTEXT; mech_oid = &krb5_oid_desc; // see whether we can use the SPNEGO mechanism major_status = gss_indicate_mechs(&minor_status, &mech_set); if (GSS_ERROR(major_status)) { kDebug(7113) << "gss_indicate_mechs failed: " << gssError(major_status, minor_status); } else { for (uint i = 0; i < mech_set->count; i++) { tmp_oid = &mech_set->elements[i]; if (tmp_oid->length == spnego_oid_desc.length && !memcmp(tmp_oid->elements, spnego_oid_desc.elements, tmp_oid->length)) { kDebug(7113) << "found SPNEGO mech"; mech_oid = &spnego_oid_desc; break; } } gss_release_oid_set(&minor_status, &mech_set); } // the service name is "HTTP/f.q.d.n" QByteArray servicename = "HTTP@"; servicename += m_resource.host().toLatin1(); input_token.value = (void *)servicename.data(); input_token.length = servicename.length() + 1; major_status = gss_import_name(&minor_status, &input_token, GSS_C_NT_HOSTBASED_SERVICE, &server); input_token.value = NULL; input_token.length = 0; if (GSS_ERROR(major_status)) { kDebug(7113) << "gss_import_name failed: " << gssError(major_status, minor_status); m_isError = true; return; } OM_uint32 req_flags; if (m_config && m_config->readEntry("DelegateCredentialsOn", false)) req_flags = GSS_C_DELEG_FLAG; else req_flags = 0; // GSSAPI knows how to get the credentials its own way, so don't ask for any major_status = gss_init_sec_context(&minor_status, GSS_C_NO_CREDENTIAL, &ctx, server, mech_oid, req_flags, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, GSS_C_NO_BUFFER, NULL, &output_token, NULL, NULL); if (GSS_ERROR(major_status) || (output_token.length == 0)) { kDebug(7113) << "gss_init_sec_context failed: " << gssError(major_status, minor_status); gss_release_name(&minor_status, &server); if (ctx != GSS_C_NO_CONTEXT) { gss_delete_sec_context(&minor_status, &ctx, GSS_C_NO_BUFFER); ctx = GSS_C_NO_CONTEXT; } m_isError = true; return; } m_headerFragment = "Negotiate "; m_headerFragment += QByteArray::fromRawData(static_cast(output_token.value), output_token.length).toBase64(); m_headerFragment += "\r\n"; // free everything gss_release_name(&minor_status, &server); if (ctx != GSS_C_NO_CONTEXT) { gss_delete_sec_context(&minor_status, &ctx, GSS_C_NO_BUFFER); ctx = GSS_C_NO_CONTEXT; } gss_release_buffer(&minor_status, &output_token); } #endif // HAVE_LIBGSSAPI