diff --git a/Changelog b/Changelog index 56b901405..5f8761989 100644 --- a/Changelog +++ b/Changelog @@ -19,6 +19,8 @@ - BUGFIX: Use XDG folders (.cache, .local) instead of .qbittorrent - BUGFIX: Added legal notice on startup that the user must accept - BUGFIX: Protect Web UI authentication against brute forcing + - BUGFIX: Use HTTP digest mode for Web UI authentication (instead of Basic) + - BUGFIX: Properly display torrents with one file in subfolder(s) - COSMETIC: Use checkboxes to filter torrent content instead of comboboxes - COSMETIC: Use alternating row colors in transfer list (set in program preferences) - COSMETIC: Added a spin box to speed limiting dialog for manual input diff --git a/src/httpconnection.cpp b/src/httpconnection.cpp index 07e76031b..c578a1775 100644 --- a/src/httpconnection.cpp +++ b/src/httpconnection.cpp @@ -137,14 +137,14 @@ void HttpConnection::respond() { write(); return; } - QStringList auth = parser.value("Authorization").split(" ", QString::SkipEmptyParts); - if (auth.size() != 2 || QString::compare(auth[0], "Basic", Qt::CaseInsensitive) != 0 || !parent->isAuthorized(auth[1].toLocal8Bit())) { + QString auth = parser.value("Authorization"); + if (QString::compare(auth.split(" ").first(), "Digest", Qt::CaseInsensitive) != 0 || !parent->isAuthorized(auth.toLocal8Bit(), parser.method())) { // Update failed attempt counter parent->client_failed_attempts.insert(socket->peerAddress().toString(), nb_fail+1); qDebug("client IP: %s (%d failed attempts)", socket->peerAddress().toString().toLocal8Bit().data(), nb_fail); // Return unauthorized header generator.setStatusLine(401, "Unauthorized"); - generator.setValue("WWW-Authenticate", "Basic realm=\"you know what\""); + generator.setValue("WWW-Authenticate", "Digest realm=\""+QString(QBT_REALM)+"\", nonce=\""+parent->generateNonce()+"\", algorithm=\"MD5\", qop=\"auth\""); write(); return; } diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 1f63cc755..6679da545 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -33,13 +33,14 @@ #include "httpconnection.h" #include "eventmanager.h" #include "bittorrent.h" -#include "preferences.h" #include #include +#include +#include HttpServer::HttpServer(Bittorrent *_BTSession, int msec, QObject* parent) : QTcpServer(parent) { username = Preferences::getWebUiUsername().toLocal8Bit(); - password_md5 = Preferences::getWebUiPassword().toLocal8Bit(); + password_ha1 = Preferences::getWebUiPassword().toLocal8Bit(); connect(this, SIGNAL(newConnection()), this, SLOT(newHttpConnection())); BTSession = _BTSession; manager = new EventManager(this, BTSession); @@ -118,21 +119,110 @@ void HttpServer::onTimer() { } } -void HttpServer::setAuthorization(QString _username, QString _password_md5) { +QString HttpServer::generateNonce() const { + QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(QTime::currentTime().toString("hhmmsszzz").toLocal8Bit()); + md5.addData(":"); + md5.addData(QBT_REALM); + return md5.result().toHex(); +} + +void HttpServer::setAuthorization(QString _username, QString _password_ha1) { username = _username.toLocal8Bit(); - password_md5 = _password_md5.toLocal8Bit(); + password_ha1 = _password_ha1.toLocal8Bit(); } -bool HttpServer::isAuthorized(QByteArray auth) const { - // Decode Auth - QByteArray decoded = QByteArray::fromBase64(auth); - QList creds = decoded.split(':'); - if(creds.size() != 2) return false; - QByteArray prop_username = creds.first(); - if(prop_username != username) return false; - QCryptographicHash md5(QCryptographicHash::Md5); - md5.addData(creds.last()); - return (password_md5 == md5.result().toHex()); +// AUTH string is: Digest username="chris", +// realm="Web UI Access", +// nonce="570d04de93444b7fd3eaeaecb00e635e", +// uri="/", algorithm=MD5, +// response="ba886766d19b45313c0e2195e4344264", +// qop=auth, nc=00000001, cnonce="e8ac970779c17075" +bool HttpServer::isAuthorized(QByteArray auth, QString method) const { + qDebug("AUTH string is %s", auth.data()); + // Get user name + QRegExp regex_user(".*username=\"([^\"]+)\".*"); + if(regex_user.indexIn(auth) < 0) return false; + QString prop_user = regex_user.cap(1); + qDebug("AUTH: Proposed username is %s, real username is %s", prop_user.toLocal8Bit().data(), username.data()); + if(prop_user != username) { + // User name is invalid, we can reject already + qDebug("AUTH-PROB: Username is invalid"); + return false; + } + // Get realm + QRegExp regex_realm(".*realm=\"([^\"]+)\".*"); + if(regex_realm.indexIn(auth) < 0) { + qDebug("AUTH-PROB: Missing realm"); + return false; + } + QByteArray prop_realm = regex_realm.cap(1).toLocal8Bit(); + if(prop_realm != QBT_REALM) { + qDebug("AUTH-PROB: Wrong realm"); + return false; + } + // get nonce + QRegExp regex_nonce(".*nonce=\"([^\"]+)\".*"); + if(regex_nonce.indexIn(auth) < 0) { + qDebug("AUTH-PROB: missing nonce"); + return false; + } + QByteArray prop_nonce = regex_nonce.cap(1).toLocal8Bit(); + qDebug("prop nonce is: %s", prop_nonce.data()); + // get uri + QRegExp regex_uri(".*uri=\"([^\"]+)\".*"); + if(regex_uri.indexIn(auth) < 0) { + qDebug("AUTH-PROB: Missing uri"); + return false; + } + QByteArray prop_uri = regex_uri.cap(1).toLocal8Bit(); + qDebug("prop uri is: %s", prop_uri.data()); + // get response + QRegExp regex_response(".*response=\"([^\"]+)\".*"); + if(regex_response.indexIn(auth) < 0) { + qDebug("AUTH-PROB: Missing response"); + return false; + } + QByteArray prop_response = regex_response.cap(1).toLocal8Bit(); + qDebug("prop response is: %s", prop_response.data()); + // Compute correct reponse + QCryptographicHash md5_ha2(QCryptographicHash::Md5); + md5_ha2.addData(method.toLocal8Bit() + ":" + prop_uri); + QByteArray ha2 = md5_ha2.result().toHex(); + QByteArray response = ""; + if(auth.contains("qop=")) { + QCryptographicHash md5_ha(QCryptographicHash::Md5); + // Get nc + QRegExp regex_nc(".*nc=(\\w+).*"); + if(regex_nc.indexIn(auth) < 0) { + qDebug("AUTH-PROB: qop but missing nc"); + return false; + } + QByteArray prop_nc = regex_nc.cap(1).toLocal8Bit(); + qDebug("prop nc is: %s", prop_nc.data()); + QRegExp regex_cnonce(".*cnonce=\"([^\"]+)\".*"); + if(regex_cnonce.indexIn(auth) < 0) { + qDebug("AUTH-PROB: qop but missing cnonce"); + return false; + } + QByteArray prop_cnonce = regex_cnonce.cap(1).toLocal8Bit(); + qDebug("prop cnonce is: %s", prop_cnonce.data()); + QRegExp regex_qop(".*qop=(\\w+).*"); + if(regex_qop.indexIn(auth) < 0) { + qDebug("AUTH-PROB: missing qop"); + return false; + } + QByteArray prop_qop = regex_qop.cap(1).toLocal8Bit(); + qDebug("prop qop is: %s", prop_qop.data()); + md5_ha.addData(password_ha1+":"+prop_nonce+":"+prop_nc+":"+prop_cnonce+":"+prop_qop+":"+ha2); + response = md5_ha.result().toHex(); + } else { + QCryptographicHash md5_ha(QCryptographicHash::Md5); + md5_ha.addData(password_ha1+":"+prop_nonce+":"+ha2); + response = md5_ha.result().toHex(); + } + qDebug("AUTH: comparing reponses"); + return prop_response == response; } EventManager* HttpServer::eventManager() const diff --git a/src/httpserver.h b/src/httpserver.h index f34069fca..373708efe 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -36,6 +36,7 @@ #include #include #include +#include "preferences.h" class Bittorrent; class QTimer; @@ -46,7 +47,7 @@ class HttpServer : public QTcpServer { private: QByteArray username; - QByteArray password_md5; + QByteArray password_ha1; Bittorrent *BTSession; EventManager *manager; QTimer *timer; @@ -54,9 +55,10 @@ class HttpServer : public QTcpServer { public: HttpServer(Bittorrent *BTSession, int msec, QObject* parent = 0); ~HttpServer(); - void setAuthorization(QString username, QString password_md5); - bool isAuthorized(QByteArray auth) const; + void setAuthorization(QString username, QString password_ha1); + bool isAuthorized(QByteArray auth, QString method) const; EventManager *eventManager() const; + QString generateNonce() const; QHash client_failed_attempts; private slots: diff --git a/src/preferences.h b/src/preferences.h index 76fb43e73..bb89c46c0 100644 --- a/src/preferences.h +++ b/src/preferences.h @@ -36,6 +36,8 @@ #include #include +#define QBT_REALM "Web UI Access" + class Preferences { public: // General options @@ -708,9 +710,10 @@ public: if(current_pass_md5 == new_password) return; // Encode to md5 and save QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":"); md5.addData(new_password.toLocal8Bit()); QSettings settings("qBittorrent", "qBittorrent"); - settings.setValue("Preferences/WebUI/Password_md5", md5.result().toHex()); + settings.setValue("Preferences/WebUI/Password_ha1", md5.result().toHex()); } static QString getWebUiPassword() { @@ -720,18 +723,20 @@ public: QString clear_pass = settings.value("Preferences/WebUI/Password", "adminadmin").toString(); settings.remove("Preferences/WebUI/Password"); QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":"); md5.addData(clear_pass.toLocal8Bit()); QString pass_md5(md5.result().toHex()); - settings.setValue("Preferences/WebUI/Password_md5", pass_md5); + settings.setValue("Preferences/WebUI/Password_ha1", pass_md5); return pass_md5; } - QString pass_md5 = settings.value("Preferences/WebUI/Password_md5", "").toString(); - if(pass_md5.isEmpty()) { + QString pass_ha1 = settings.value("Preferences/WebUI/Password_ha1", "").toString(); + if(pass_ha1.isEmpty()) { QCryptographicHash md5(QCryptographicHash::Md5); + md5.addData(getWebUiUsername().toLocal8Bit()+":"+QBT_REALM+":"); md5.addData("adminadmin"); - pass_md5 = md5.result().toHex(); + pass_ha1 = md5.result().toHex(); } - return pass_md5; + return pass_ha1; } };