diff --git a/src/bridge/AuthorizationManager.cpp b/src/bridge/AuthorizationManager.cpp new file mode 100644 index 0000000..ce33122 --- /dev/null +++ b/src/bridge/AuthorizationManager.cpp @@ -0,0 +1,247 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include "AuthorizationManager.h" + +#include "globals.h" + +// Stuff for PAM to work +#include +#include +#include +#include +#include +#include +#include +#include + +//Stuff for OpenSSL to work +#include +#include +#include +#include +#include +#include + +//Internal defines +// -- token management +#define TIMEOUTSECS 900 // (15 minutes) time before a token becomes invalid +#define AUTHCHARS QString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +#define TOKENLENGTH 40 + +AuthorizationManager::AuthorizationManager() : QObject(){ + HASH.clear(); + IPFAIL.clear(); + //initialize the random number generator (need to generate auth tokens) + qsrand(QDateTime::currentMSecsSinceEpoch()); +} + +AuthorizationManager::~AuthorizationManager(){ + +} + +// == Token Interaction functions == +void AuthorizationManager::clearAuth(QString token){ + if(token.isEmpty() || token.length() < TOKENLENGTH){ return; } //not a valid token + //clear an authorization token + QString id = hashID(token); + //qDebug() << "Clear Auth:" << id; + if(!id.isEmpty()){ HASH.remove(id); } +} + +bool AuthorizationManager::checkAuth(QString token){ + //see if the given token is valid + bool ok = false; + QString id = hashID(token); + if(!id.isEmpty()){ + //Also verify that the token has not timed out + ok = (HASH[id] > QDateTime::currentDateTime()); + if(ok){ HASH.insert(id, QDateTime::currentDateTime().addSecs(TIMEOUTSECS)); } //valid - bump the timestamp + } + return ok; +} + +//SSL Certificate register/revoke/list +bool AuthorizationManager::RegisterCertificate(QString token, QString pubkey, QString nickname, QString email){ + if(!checkAuth(token)){ return false; } + QString user = hashID(token).section("::::",2,2); //get the user name from the currently-valid token + //NOTE: The public key should be a base64 encoded string + CONFIG->setValue("RegisteredCerts/"+user+"/"+pubkey, "Nickname: "+nickname+"\nEmail: "+email+"\nDate Registered: "+QDateTime::currentDateTime().toString(Qt::ISODate) ); + return true; +} + +bool AuthorizationManager::RevokeCertificate(QString token, QString key, QString user){ + //user will be the current user if not empty - cannot touch other user's certs without full perms on current session + QString cuser = hashID(token).section("::::",2,2); + + //Check that the given cert exists first + if( !CONFIG->contains("RegisteredCerts/"+cuser+"/"+key) ){ return false; } + CONFIG->remove("RegisteredCerts/"+cuser+"/"+key); + return true; +} + +void AuthorizationManager::ListCertificates(QString token, QJsonObject *out){ + QStringList keys; //Format: "RegisteredCerts//" + //Read all user's certs + keys = CONFIG->allKeys().filter("RegisteredCerts/"); + //qDebug() << "Found SSL Keys to List:" << keys; + + keys.sort(); + //Now put the known keys into the output structure arranged by username/key + QJsonObject user; QString username; + for(int i=0; iinsert(username, user); user = QJsonObject(); } //save the current info to the output + username = keys[i].section("/",1,1); //save the new username for later + } + QString info = CONFIG->value(keys[i]).toString(); + QString key = keys[i].section("/",2,-1);//just in case the key has additional "/" in it + user.insert(key,info); + } + if(!user.isEmpty() && !username.isEmpty()){ out->insert(username, user); } +} + +//Generic functions +int AuthorizationManager::checkAuthTimeoutSecs(QString token){ + //Return the number of seconds that a token is valid for + if(!HASH.contains(token)){ return 0; } //invalid token + return QDateTime::currentDateTime().secsTo( HASH[token] ); +} + + +// == Token Generation functions + +//Stage 1 SSL Login Check: Generation of random string for this user +QString AuthorizationManager::GenerateEncCheckString(){ + QString key; + for(int i=0; i HASH[pubkeys[i]]){ + //Note: normally only 1 request per user at a time, but it is possible for a couple different clients to try + // and authenticate as the same user (but different keys) at nearly the same time - so keep a short valid-string time frame (<30 seconds) + // to mitigate this possibility (need to prevent the second user-auth request from invalidating the first before the first auth handshake is finished) + HASH.remove(pubkeys[i]); //initstring expired - go ahead and remove it to reduce calc time later + } + } + //Now re-use the "pubkeys" variable for the public SSL keys + QString user; + pubkeys = CONFIG->allKeys().filter("RegisteredCerts/"); //Format: "RegisteredCerts//" + //qDebug() << " - Check pubkeys";// << pubkeys; + for(int i=0; i::::" + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + int fails = 0; + if(!keys.isEmpty()){ + //Take the existing key/value and put a new one in (this limits the filter to 1 value maximum) + QDateTime last = IPFAIL.take(keys[0]); + if(last.addSecs(CONFIG->value("blacklist_settings/block_minutes",60).toInt()*60) > QDateTime::currentDateTime() ){ + fails = keys[0].section("::::",1,1).toInt(); + } + } + fails++; + IPFAIL.insert(host+"::::"+QString::number(fails), QDateTime::currentDateTime() ); + return (fails>=CONFIG->value("blacklist_settings/fails_to_block",2).toInt()); +} + +void AuthorizationManager::ClearHostFail(QString host){ + QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); + for(int i=0; i July 2015 +// ================================= +#ifndef _PCBSD_REST_AUTHORIZATION_MANAGER_H +#define _PCBSD_REST_AUTHORIZATION_MANAGER_H + +#include +#include +#include +#include +#include +#include + +class AuthorizationManager : public QObject{ + Q_OBJECT +public: + AuthorizationManager(); + ~AuthorizationManager(); + + // == Token Interaction functions == + void clearAuth(QString token); //clear an authorization token + bool checkAuth(QString token); //see if the given token is valid + + //SSL Certificate register/revoke/list (should only run if the current token is valid) + bool RegisterCertificate(QString token, QString pubkey, QString nickname, QString email); //if token is valid, register the given cert for future logins + bool RevokeCertificate(QString token, QString key, QString user=""); //user will be the current user if not empty - cannot touch other user's certs without full perms on current session + void ListCertificates(QString token, QJsonObject *out); + + int checkAuthTimeoutSecs(QString token); //Return the number of seconds that a token is valid for + + //Stage 1 SSL Login Check: Generation of random string for this user + QString GenerateEncCheckString(); + //Stage 2 SSL Login Check: Verify that the returned/encrypted string can be decoded and matches the initial random string + QString LoginUC(QHostAddress host, QString encstring); + + +private: + QHash HASH; + QHash IPFAIL; + + QString generateNewToken(bool isOperator, QString name); + + //Failure count management + bool BumpFailCount(QString host); + void ClearHostFail(QString host); + + //token->hashID filter simplification + QString hashID(QString token){ + QStringList tmp = QStringList(HASH.keys()).filter(token+"::::"); + if(tmp.isEmpty()){ return ""; } + else{ return tmp.first(); } + } + + //SSL Decrypt function + QString DecryptSSLString(QString encstring, QString pubkey); + +signals: + void BlockHost(QHostAddress); //block a host address temporarily + +}; + +#endif diff --git a/src/bridge/BridgeConnection.cpp b/src/bridge/BridgeConnection.cpp index 32b2e40..151feec 100644 --- a/src/bridge/BridgeConnection.cpp +++ b/src/bridge/BridgeConnection.cpp @@ -77,7 +77,7 @@ QStringList BridgeConnection::JsonArrayToStringList(QJsonArray array){ void BridgeConnection::InjectMessage(QString msg){ //See if this message is directed to the bridge itself, or a client - if(msg.startsWith("{")){ + if(msg.startsWith("{") || !AUTHSYSTEM->checkAuth(SockAuthToken) ){ HandleAPIMessage(msg); }else{ //Need to read the destination off the message first @@ -119,10 +119,35 @@ void BridgeConnection::HandleAPIMessage(QString msg){ out.insert("id", JM.value("id")); out.insert("namespace", namesp); out.insert("name","reponse"); - QJsonObject outargs; + QJsonValue outargs; //There is only a short list of API calls the bridge is capable of: if(namesp == "rpc" && name=="identify"){ - outargs.insert("type","bridge"); + QJsonObject tmp; + tmp.insert("type","bridge"); + outargs = tmp; + + }else if(namesp == "rpc" && name=="auth_ssl"){ + if(!args.contains("encrypted_string")){ + //Stage 1 - send a random string to encrypt + QString key = AUTHSYSTEM->GenerateEncCheckString(); + QJsonObject obj; obj.insert("test_string", key); + outargs = obj; + }else{ + //Stage 2 - verify returned encrypted string + SockAuthToken = AUTHSYSTEM->LoginUC(SOCKET->peerAddress(),args.value("encrypted_string").toString() ); + if(AUTHSYSTEM->checkAuth(SockAuthToken)){ + QJsonArray array; + array.append(SockAuthToken); + array.append(AUTHSYSTEM->checkAuthTimeoutSecs(SockAuthToken)); + outargs = array; + }else{ + out.insert("name","error"); + outargs = "unauthorized"; + } + } + }else if(AUTHSYSTEM->checkAuth(SockAuthToken)){ + //Valid auth - a couple more API calls available here + }else{ out.insert("name","error"); //unknown API call } @@ -142,10 +167,10 @@ void BridgeConnection::checkIdle(){ } void BridgeConnection::checkAuth(){ - //if(!AUTHSYSTEM->checkAuth(SockAuthToken)){ + if(!AUTHSYSTEM->checkAuth(SockAuthToken)){ //Still not authorized - disconnect - //checkIdle(); - //} + checkIdle(); + } } void BridgeConnection::SocketClosing(){ diff --git a/src/bridge/BridgeConnection.h b/src/bridge/BridgeConnection.h index 0e7ab70..9bc4f8d 100644 --- a/src/bridge/BridgeConnection.h +++ b/src/bridge/BridgeConnection.h @@ -30,6 +30,7 @@ private: void InjectMessage(QString msg); void HandleAPIMessage(QString msg); + private slots: void checkIdle(); //see if the currently-connected client is idle void checkAuth(); //see if the currently-connected client has authed yet diff --git a/src/bridge/BridgeServer.cpp b/src/bridge/BridgeServer.cpp index 1cc5bb9..3adf362 100644 --- a/src/bridge/BridgeServer.cpp +++ b/src/bridge/BridgeServer.cpp @@ -13,7 +13,7 @@ BridgeServer::BridgeServer() : QWebSocketServer("sysadm-bridge", QWebSocketServer::SecureMode){ //Setup all the various settings //AUTH = new AuthorizationManager(); - //connect(AUTH, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); + connect(AUTHSYSTEM, SIGNAL(BlockHost(QHostAddress)), this, SLOT(BlackListConnection(QHostAddress)) ); } BridgeServer::~BridgeServer(){ @@ -92,7 +92,7 @@ bool BridgeServer::allowConnection(QHostAddress addr){ if(!CONFIG->contains(key) ){ return true; } //not in the list //Address on the list - see if the timeout has expired QDateTime dt = CONFIG->value(key,QDateTime()).toDateTime(); - if(dt.addSecs(CONFIG->value("blacklist_settings/blockmins",60).toInt()*60) < QDateTime::currentDateTime()){ + if(dt.addSecs(CONFIG->value("blacklist_settings/block_minutes",60).toInt()*60) < QDateTime::currentDateTime()){ //This entry has timed out - go ahead and allow it CONFIG->remove(key); //make the next connection check for this IP faster again return true; diff --git a/src/bridge/bridge.pro b/src/bridge/bridge.pro index 8d8c277..99d41c0 100644 --- a/src/bridge/bridge.pro +++ b/src/bridge/bridge.pro @@ -6,12 +6,13 @@ QT = core network websockets HEADERS += globals.h \ BridgeServer.h \ - BridgeConnection.h + BridgeConnection.h \ + AuthorizationManager.h SOURCES += main.cpp \ BridgeServer.cpp \ - BridgeConnection.cpp - + BridgeConnection.cpp \ + AuthorizationManager.cpp TARGET=sysadm-bridge @@ -25,4 +26,4 @@ INSTALLS += target scripts QMAKE_LIBDIR = /usr/local/lib/qt5 /usr/local/lib INCLUDEPATH += /usr/local/include -LIBS += -L/usr/local/lib -lpam -lutil -lssl -lcrypto +LIBS += -L/usr/local/lib -lutil -lssl -lcrypto diff --git a/src/bridge/globals.h b/src/bridge/globals.h index 890eaa4..a4a2a4c 100644 --- a/src/bridge/globals.h +++ b/src/bridge/globals.h @@ -37,4 +37,9 @@ #define SETTINGSFILE "/var/db/sysadm-bridge.ini" +#define SYSADM_BRIDGE +#include "AuthorizationManager.h" + extern QSettings* CONFIG; +extern AuthorizationManager* AUTHSYSTEM; + diff --git a/src/bridge/main.cpp b/src/bridge/main.cpp index 4a7a9aa..012a6d4 100644 --- a/src/bridge/main.cpp +++ b/src/bridge/main.cpp @@ -12,9 +12,9 @@ #define DEBUG 0 -//Create any global classes +//Create any global classes/settings QSettings *CONFIG = new QSettings(SETTINGSFILE, QSettings::IniFormat); - +AuthorizationManager *AUTHSYSTEM = new AuthorizationManager(); //Set the defail values for the global config variables /*int BlackList_BlockMinutes = 60; @@ -59,7 +59,9 @@ int main( int argc, char ** argv ) if(!info.contains("=")){ continue; } //invalid format QString var = info.section("=",0,0); QString val = info.section("=",1,-1); qDebug() << "Changing bridge setting:" << info; - if(var=="blacklist/blockmins"){ CONFIG->setValue("blacklist_settings/blockmins",val.toInt()); } + if(var=="blacklist/block_minutes"){ CONFIG->setValue("blacklist_settings/block_minutes",val.toInt()); } + else if(var=="blacklist/fails_to_block"){ CONFIG->setValue("blacklist_settings/fails_to_block",val.toInt()); } + //else if(var=="blacklist_settings/block_minutes"){ CONFIG->setValue("blacklist_settings/block_minutes",val.toInt()); } } else if( (QString(argv[i])=="-port" || QString(argv[i])=="-p") && (i+1