// =============================== // 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" #include #include "library/sysadm-general.h" //simplification functions // 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 20 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; } bool AuthorizationManager::hasFullAccess(QString token){ bool ok = false; QString id = hashID(token); if(!id.isEmpty()){ //Also verify that the token has not timed out if( HASH[id] > QDateTime::currentDateTime() ){ ok = id.section("::::",1,1)=="operator"; } } 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::RegisterCertificateInternal(QString user, QByteArray pubkey, QString nickname, QString email){ pubkey = pubkey.toBase64(); //NOTE: The public key should be saved as 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); if(user.isEmpty()){ user = cuser; } //only probe current user if(user !=cuser){ //Check permissions for this cross-user action if(!hasFullAccess(token)){ return false; } } //Check that the given cert exists first if( !CONFIG->contains("RegisteredCerts/"+user+"/"+key) ){ return false; } CONFIG->remove("RegisteredCerts/"+user+"/"+key); return true; } void AuthorizationManager::ListCertificates(QString token, QJsonObject *out){ QStringList keys; //Format: "RegisteredCerts//" if( hasFullAccess(token) ){ //Read all user's certs keys = CONFIG->allKeys().filter("RegisteredCerts/"); //qDebug() << "Found SSL Keys to List:" << keys; }else{ //Only list certs for current user QString cuser = hashID(token).section("::::",2,2); keys = CONFIG->allKeys().filter("RegisteredCerts/"+cuser+"/"); //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); } } void AuthorizationManager::ListCertificateChecksums(QJsonObject *out){ QStringList keys; //Format: "RegisteredCerts//" (value is full text) //Read all user's certs (since we only need checksums) keys = CONFIG->allKeys().filter("RegisteredCerts/"); keys.sort(); QJsonArray arr; QCryptographicHash chash(QCryptographicHash::Md5); //qDebug() << "MD5 Generation:"; for(int i=0; iinsert("md5_keys", arr); } //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 QString AuthorizationManager::LoginUP(QHostAddress host, QString user, QString pass){ //Login w/ username & password bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) || (host.toString()=="::ffff:127.0.0.1") ); bool ok = false; bool isOperator = false; //First check that the user is valid on the system and part of the operator group if( CONFIG->value("auth/allowUserPassAuth",true).toBool() ){ if(user!="root" && user!="toor"){ QStringList groups = getUserGroups(user); if(groups.contains("wheel")){ isOperator = true; } //full-access user else if(!groups.contains("operator")){ return ""; //user not allowed access if not in either of the wheel/operator groups } }else{ isOperator = true; } //qDebug() << "Check username/password" << user << pass << localhost; //Need to run the full username/password through PAM if(!localhost || user=="root" || user=="toor"){ ok = pam_checkPW(user,pass); }else{ ok = local_checkActive(user); //allow local access for users without password } } qDebug() << "User Login Attempt:" << user << " Success:" << ok << " IP:" << host.toString(); LogManager::log(LogManager::HOST, QString("User Login Attempt: ")+user+" Success: "+(ok?"true":"false")+" IP: "+host.toString() ); if(!ok){ //invalid login //Bump the fail count for this host bool overlimit = BumpFailCount(host.toString()); if(overlimit){ emit BlockHost(host); } return (overlimit ? "REFUSED" : ""); }else{ //valid login - generate a new token for it ClearHostFail(host.toString()); return generateNewToken(isOperator, user); } } QString AuthorizationManager::LoginService(QHostAddress host, QString service){ bool localhost = ( (host== QHostAddress::LocalHost) || (host== QHostAddress::LocalHostIPv6) || (host.toString()=="::ffff:127.0.0.1") ); //Login a particular automated service qDebug() << "Service Login Attempt:" << service << " Success:" << localhost; if(!localhost){ return ""; } //invalid - services must be local for access //Check that the service is valid on the system bool isok = localhost && CONFIG->value("auth/allowServiceAuth",false).toBool( ); if(service!="root" && service!="toor" && isok){ QStringList groups = getUserGroups(service); isok = (groups.contains(service) && !groups.contains("wheel") && !groups.contains("operator")); } //Now generate a new token and send it back if(!isok){ //invalid login if(!localhost){ //Bump the fail count for this host bool overlimit = BumpFailCount(host.toString()); if(overlimit){ emit BlockHost(host); } return (overlimit ? "REFUSED" : ""); }else{ return ""; } }else{ return generateNewToken(false, service); }//services are never given operator privileges } //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; i1){ outstring = QJsonDocument(array).toJson(QJsonDocument::Compact); } }else{ //Using PUBLIC key to encrypt rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa,NULL, NULL); if(rsa==NULL){ qDebug() << " - Bad rsa"; BIO_free_all(keybio); return ""; } QJsonArray array; int rsa_size = RSA_size(rsa)/2; for(int i=0; i1){ outstring = QJsonDocument(array).toJson(QJsonDocument::Compact); } } return outstring; } QString AuthorizationManager::decryptString(QString str, QByteArray key){ bool pub=true; if(key.contains(" PUBLIC KEY--")){ pub=true; } else if(key.contains(" PRIVATE KEY--")){ pub=false; } else{ //unknown encryption - just return as-is if(!key.isEmpty()){ qDebug() << "Unknown key type!!" << key; } return str; } //Convert the input string into block elements as needed (and decode base64); QList blocks; QJsonDocument doc = QJsonDocument::fromJson(str.toLocal8Bit()); if(doc.isNull()){ //No individual blocks - just one string QByteArray bytes; bytes.append(str); blocks << QByteArray::fromBase64(bytes); }else if(doc.isArray()){ for(int i=0; i AuthorizationManager::GenerateSSLKeyPair(){ const int kBits = 4096; const int kExp = 3; char *pem_key, *pem_key_pub; RSA *rsa = RSA_generate_key(kBits, kExp, 0, 0); //Private key in PEM form: BIO *bio = BIO_new(BIO_s_mem()); PEM_write_bio_RSAPrivateKey(bio, rsa, NULL, NULL, 0, NULL, NULL); int keylen = BIO_pending(bio); pem_key = (char *)malloc(keylen); /* Null-terminate */ BIO_read(bio, pem_key, keylen); QByteArray privkey = QByteArray::fromRawData(pem_key, keylen); //Public key in PEM form: BIO *bio2 = BIO_new(BIO_s_mem()); PEM_write_bio_RSA_PUBKEY(bio2, rsa); int keylen2 = BIO_pending(bio2); pem_key_pub = (char *)malloc(keylen2); /* Null-terminate */ BIO_read(bio2, pem_key_pub, keylen2); QByteArray pubkey = QByteArray::fromRawData(pem_key_pub, keylen2); BIO_free_all(bio); BIO_free_all(bio2); RSA_free(rsa); //See if the keys can be loaded for use later /*RSA *rsa1= NULL; BIO *keybio1 = NULL; //qDebug() << " - Generate keybio"; keybio1 = BIO_new_mem_buf(pubkey.data(), -1); if(keybio1!=NULL){ rsa1 = PEM_read_bio_RSA_PUBKEY(keybio1, &rsa1,NULL, NULL); qDebug() << "Can read new public key:" << (rsa1!=NULL); RSA_free(rsa1); } BIO_free_all(keybio1); RSA *rsa2= NULL; BIO *keybio2 = NULL; //qDebug() << " - Generate keybio"; keybio2 = BIO_new_mem_buf(privkey.data(), -1); if(keybio2!=NULL){ rsa2 = PEM_read_bio_RSAPrivateKey(keybio2, &rsa1,NULL, NULL); qDebug() << "Can read new private key:" << (rsa2!=NULL); RSA_free(rsa2); } BIO_free_all(keybio2); */ return (QList() << pubkey << privkey); } QByteArray AuthorizationManager::pubkeyForMd5(QString md5_base64){ QByteArray md5 = QByteArray::fromBase64( md5_base64.toLocal8Bit() ); QStringList keys; //Format: "RegisteredCerts//" (value is full text) //Read all user's certs (since we only need checksums) keys = CONFIG->allKeys().filter("RegisteredCerts/"); keys.sort(); QJsonArray arr; QCryptographicHash chash(QCryptographicHash::Md5); //qDebug() << "MD5 Generation:"; for(int i=0; i2); //more than 1 active process for this user (labels + shell/desktop + tool used to communicate with sysadm) }else{ //No X sessions - look for normal login sessions QStringList active = sysadm::General::RunCommand("users").section("\n",0,0).split(" "); //qDebug() << "active users" << active; return active.contains(user); } } bool AuthorizationManager::BumpFailCount(QString host){ //Returns: true if the failure count is over the limit //key: "::::" 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(BlackList_AuthFailResetMinutes*60) > QDateTime::currentDateTime() ){ fails = keys[0].section("::::",1,1).toInt(); } } fails++; IPFAIL.insert(host+"::::"+QString::number(fails), QDateTime::currentDateTime() ); return (fails>=BlackList_AuthFailsToBlock); } void AuthorizationManager::ClearHostFail(QString host){ QStringList keys = QStringList(IPFAIL.keys()).filter(host+"::::"); for(int i=0; i