#include "User.h" #include "UserBackup.h" #include "sodium.h" #include "../SingletonManager/SessionManager.h" #include "../SingletonManager/ErrorManager.h" #include "../SingletonManager/SingletonTaskObserver.h" #include "Group.h" #include "../lib/DataTypeConverter.h" #include "../tasks/VerificationEmailResendTask.h" #include "../ServerConfig.h" #include "Poco/Timestamp.h" namespace controller { User::User(model::table::User* dbModel) : mPassword(nullptr), mGradidoKeyPair(nullptr), mCanDecryptPrivateKey(false), mGradidoCurrentBalance(0) { mDBModel = dbModel; } User::~User() { if (mGradidoKeyPair) { delete mGradidoKeyPair; mGradidoKeyPair = nullptr; } } Poco::AutoPtr User::create() { auto db = new model::table::User(); auto user = new User(db); return Poco::AutoPtr(user); } Poco::AutoPtr User::create(const std::string& email, const std::string& first_name, const std::string& last_name, int group_id, Poco::UInt64 passwordHashed/* = 0*/, std::string languageKey/* = "de"*/) { auto db = new model::table::User(email, first_name, last_name, group_id, passwordHashed, languageKey); auto user = new User(db); return Poco::AutoPtr(user); } std::vector User::search(const std::string& searchString, const std::string& accountState /* = "all" */) { auto sm = SessionManager::getInstance(); auto cm = ConnectionManager::getInstance(); auto db = new model::table::User(); static const char* functionName = "User::search"; std::string globalSearch = "%" + searchString + "%"; std::vector resultFromDB; if (accountState == "email not activated") { std::vector fieldNames = { "first_name", "last_name", "email", "email_checked" }; auto session = cm->getConnection(CONNECTION_MYSQL_LOGIN_SERVER); std::vector results; using namespace Poco::Data::Keywords; Poco::Data::Statement select(session); // typedef Poco::Tuple, Poco::DateTime, int, int, int> UserTuple; select << "SELECT id, first_name, last_name, email, username, pubkey, created, email_checked, disabled, group_id FROM " << db->getTableName(); select << " where email_checked = 0 "; select, into(resultFromDB); if (searchString != "") { select << "AND (first_name LIKE ? OR last_name LIKE ? OR email LIKE ?)"; select, useRef(globalSearch), useRef(globalSearch), useRef(globalSearch); } try { select.execute(); } catch (Poco::Exception& ex) { NotificationList errors; errors.addError(new ParamError(functionName, "mysql error ", ex.displayText())); errors.addError(new ParamError(functionName, "search string", searchString)); errors.addError(new ParamError(functionName, "account state", accountState)); errors.sendErrorsAsEmail(); } } else { std::vector fieldNames = { "first_name", "last_name", "email" }; std::vector fieldValues = { globalSearch, globalSearch, globalSearch }; resultFromDB = db->loadFromDB(fieldNames, fieldValues, model::table::MYSQL_CONDITION_OR); } db->release(); db = nullptr; std::vector resultVector; resultVector.reserve(resultFromDB.size()); for (auto it = resultFromDB.begin(); it != resultFromDB.end(); it++) { resultVector.push_back(new User(new model::table::User(*it))); } return resultVector; } bool User::isUsernameAlreadyUsed(const std::string& username) { auto db = getModel(); return db->loadFromDB({ "username", "group_id" }, username, db->getGroupId(), model::table::MYSQL_CONDITION_AND) > 0; } int User::load(const unsigned char* pubkey_array) { Poco::Data::BLOB pubkey(pubkey_array, 32); return getModel()->loadFromDB("pubkey", pubkey); } int User::load(MemoryBin* emailHash) { Poco::Data::BLOB email_hash(*emailHash, crypto_generichash_BYTES); return getModel()->loadFromDB("email_hash", email_hash); } Poco::AutoPtr User::sload(int user_id) { auto db = new model::table::User(); if (0 == db->loadFromDB("id", user_id)) { delete db; return nullptr; } auto user = new User(db); return Poco::AutoPtr(user); } void User::reload() { getModel()->loadFromDB("id", getModel()->getID()); } const std::string& User::getPublicHex() { if (mPublicHex != "") { return mPublicHex; } auto mm = MemoryManager::getInstance(); lock("User::getJson"); Poco::JSON::Object userObj; auto pubkey = getModel()->getPublicKey(); if (pubkey) { auto pubkeyHex = mm->getFreeMemory(65); memset(*pubkeyHex, 0, 65); sodium_bin2hex(*pubkeyHex, 65, pubkey, 32); mPublicHex = (char*)*pubkeyHex; mm->releaseMemory(pubkeyHex); } unlock(); return mPublicHex; } std::string User::getEmailWithNames() { std::stringstream ss; auto model = getModel(); ss << model->getFirstName() << " " << model->getLastName() << "<" << model->getEmail() << ">"; return ss.str(); } Poco::JSON::Object User::getJson() { auto json = getModel()->getJson(); auto pubkey = getPublicHex(); //printf("[controller::User::getJson] this: %d\n", (int)this); if (pubkey != "") { json.set("public_hex", pubkey); } return json; } int User::login(const std::string& password) { if (!mPassword.isNull() && mPassword->hasKey()) { return 2; } auto observer = SingletonTaskObserver::getInstance(); std::unique_lock _lock(mSharedMutex); assert(mPassword.isNull()); auto model = getModel(); auto email_hash = observer->makeHash(model->getEmail()); if (observer->getTaskCount(email_hash, TASK_OBSERVER_PASSWORD_CREATION) > 0) { return -3; } observer->addTask(email_hash, TASK_OBSERVER_PASSWORD_CREATION); Poco::AutoPtr authenticated_encryption(new SecretKeyCryptography); assert(!authenticated_encryption.isNull() && model); authenticated_encryption->createKey(model->getEmail(), password); observer->removeTask(email_hash, TASK_OBSERVER_PASSWORD_CREATION); if (authenticated_encryption->getKeyHashed() == model->getPasswordHashed()) { // printf("[User::login] password key hashed is the same as saved password hash\n"); MemoryBin* clear_private_key = nullptr; mPassword = authenticated_encryption; // additional check if saved private key found, decrypt and derive public key and compare with saved public key if (!model->hasPrivateKeyEncrypted()) { return 1; } else { if (SecretKeyCryptography::AUTH_DECRYPT_OK == authenticated_encryption->decrypt(model->getPrivateKeyEncrypted(), &clear_private_key)) { if (mGradidoKeyPair) { if (mGradidoKeyPair->isTheSame(clear_private_key) == 0) { mCanDecryptPrivateKey = true; return 1; } else { delete mGradidoKeyPair; mGradidoKeyPair = nullptr; } } mGradidoKeyPair = new KeyPairEd25519(clear_private_key); if (!model->getPublicKey()) { model->setPublicKey(mGradidoKeyPair->getPublicKey()); model->updatePublickey(); } else if (!mGradidoKeyPair->isTheSame(model->getPublicKey())) { delete mGradidoKeyPair; mGradidoKeyPair = nullptr; //printf("pubkeys don't match\n"); return -1; } //printf("correct pwd\n"); mCanDecryptPrivateKey = true; return 1; } else { //printf("decrypt error\n"); return -2; } } } // password didn't match //printf("password hashed key: %ull, model pwd hashed keys: %ull\n", authenticated_encryption->getKeyHashed(), model->getPasswordHashed()); //printf("password: %d\n", (int)(mPassword.get())); return 0; } int User::setGradidoKeyPair(KeyPairEd25519* gradidoKeyPair) { assert(gradidoKeyPair); std::unique_lock _lock(mSharedMutex); if (mGradidoKeyPair) delete mGradidoKeyPair; mGradidoKeyPair = gradidoKeyPair; auto model = getModel(); model->setPublicKey(mGradidoKeyPair->getPublicKey()); if (mPassword && mPassword->hasKey()) { model->setPrivateKey(mGradidoKeyPair->getCryptedPrivKey(mPassword)); mCanDecryptPrivateKey = true; return 1; } return 0; } int User::setNewPassword(const std::string& password) { auto observer = SingletonTaskObserver::getInstance(); auto model = getModel(); auto email_hash = observer->makeHash(model->getEmail()); observer->addTask(email_hash, TASK_OBSERVER_PASSWORD_CREATION); Poco::AutoPtr authenticated_encryption(new SecretKeyCryptography); assert(!authenticated_encryption.isNull() && model); authenticated_encryption->createKey(model->getEmail(), password); observer->removeTask(email_hash, TASK_OBSERVER_PASSWORD_CREATION); return setNewPassword(authenticated_encryption); } int User::setNewPassword(Poco::AutoPtr passwd) { std::unique_lock _lock(mSharedMutex); auto model = getModel(); if (!mPassword.isNull() && !passwd.isNull()) { // if keys matched if (mPassword->isTheSame(passwd)) { return 0; } // if password exist but gradido key pair not, try to load key pair if ((!mGradidoKeyPair || !mGradidoKeyPair->hasPrivateKey()) && model->hasPrivateKeyEncrypted()) { //if (!mGradidoKeyPair) mGradidoKeyPair = new KeyPairEd25519; MemoryBin* clear_private_key = nullptr; if (SecretKeyCryptography::AUTH_DECRYPT_OK == mPassword->decrypt(model->getPrivateKeyEncrypted(), &clear_private_key)) { if (mGradidoKeyPair && mGradidoKeyPair->isTheSame(clear_private_key) != 0) { delete mGradidoKeyPair; mGradidoKeyPair = nullptr; } if (!mGradidoKeyPair) { mGradidoKeyPair = new KeyPairEd25519(clear_private_key); } // check if saved pubkey and from private key extracted pubkey match if (*mGradidoKeyPair != model->getPublicKey()) { delete mGradidoKeyPair; mGradidoKeyPair = nullptr; mCanDecryptPrivateKey = false; return -1; } } } } // replace old password with new mPassword = passwd; // set new encrypted password and hash model->setPasswordHashed(mPassword->getKeyHashed()); int result = 2; if (mGradidoKeyPair && mGradidoKeyPair->hasPrivateKey()) { auto encryptedPrivateKey = mGradidoKeyPair->getCryptedPrivKey(mPassword); model->setPrivateKey(encryptedPrivateKey); MemoryManager::getInstance()->releaseMemory(encryptedPrivateKey); result = model->updatePrivkeyAndPasswordHash(); mCanDecryptPrivateKey = true; } else { model->updateIntoDB("password", mPassword->getKeyHashed()); } // save changes to db return result; } //! \return -1 no matching entry found //! \return -2 if user id is not set or invalid //! \return 0 matching entry found int User::tryLoadPassphraseUserBackup(KeyPairEd25519** createdKeyPair/* = nullptr*/) { auto user_model = getModel(); if (user_model->getID() <= 0) return -2; auto backups = UserBackup::load(user_model->getID()); if (backups.size() == 0) return -1; for (auto it = backups.begin(); it != backups.end(); it++) { auto user_backup = *it; if (-1 == user_backup->getModel()->getMnemonicType()) { continue; } auto key_pair = user_backup->createGradidoKeyPair(); if (key_pair->isTheSame(user_model->getPublicKey())) { if (createdKeyPair) { *createdKeyPair = key_pair; } else { delete key_pair; } return 0; } delete key_pair; } return -1; } /* USER_EMPTY, USER_LOADED_FROM_DB, USER_PASSWORD_INCORRECT, USER_PASSWORD_ENCRYPTION_IN_PROCESS, USER_EMAIL_NOT_ACTIVATED, USER_NO_KEYS, USER_NO_PRIVATE_KEY, USER_NO_GROUP, USER_KEYS_DONT_MATCH, USER_COMPLETE, USER_DISABLED */ UserState User::getUserState() { std::unique_lock _lock(mSharedMutex); auto model = getModel(); if (!model->getID() && model->getEmail() == "") { return USER_EMPTY; } if (!model->hasPrivateKeyEncrypted() && !model->hasPublicKey()) { return USER_NO_KEYS; } if (!model->hasPrivateKeyEncrypted()) { return USER_NO_PRIVATE_KEY; } if (!model->getGroupId()) { return USER_NO_GROUP; } if (!model->isEmailChecked()) { return USER_EMAIL_NOT_ACTIVATED; } return USER_COMPLETE; } int User::checkIfVerificationEmailsShouldBeResend(const Poco::Util::Timer& timer) { return 0; auto cm = ConnectionManager::getInstance(); auto em = ErrorManager::getInstance(); static const char* function_name = "User::checkIfVerificationEmailsShouldBeResend"; auto session = cm->getConnection(CONNECTION_MYSQL_LOGIN_SERVER); Poco::Data::Statement select(session); std::vector> results; int email_checked = 0; int resend_count = 1; select << "select u.id, v.created from users as u " << "LEFT JOIN email_opt_in as v ON(u.id = v.user_id) " << "where u.email_checked = ? " << "AND v.resend_count <= ? " << "ORDER BY u.id, v.created " , Poco::Data::Keywords::use(email_checked), Poco::Data::Keywords::use(resend_count), Poco::Data::Keywords::into(results) ; int result_count = 0; try { result_count = select.execute(); } catch (Poco::Exception& ex) { em->addError(new ParamError(function_name, "mysql error by select", ex.displayText().data())); em->sendErrorsAsEmail(); //return -1; } //printf("result_count: %d\n", result_count); if (result_count > 0) { auto now = Poco::DateTime(); int count_scheduled_at_once = 0; int count_scheduled = 0; int last_user_id = 0; // add 1 for resend task scheduled at once // add 2 for resend task scheduled in the future // reset if new user_id came up int scheduledResendTask = 0; // results sorted by user_id //printf("results count: %d\n", results.size()); for (auto it = results.begin(); it != results.end(); it++) { auto user_id = it->get<0>(); auto created = it->get<1>(); //auto created_str = Poco::DateTimeFormatter::format(created, "%f.%m.%y %H:%M"); //printf("user_id: %d, created: %s\n", user_id, created_str.data()); if (user_id != last_user_id) { assert(user_id > last_user_id); last_user_id = user_id; scheduledResendTask = 0; } if (scheduledResendTask == 3) continue; auto age = now - created; //printf("age: %d\n", age.days()); // older than 7 days, schedule at once if (age.days() > 7 && !(scheduledResendTask & 1)) { UniLib::controller::TaskPtr verificationResendTask(new VerificationEmailResendTask(user_id)); verificationResendTask->scheduleTask(verificationResendTask); count_scheduled_at_once++; scheduledResendTask |= 1; } // younger than 7 days, schedule for created + 7 days else if(!(scheduledResendTask & 2)) { auto runDateTime = created + Poco::Timespan(7, 0, 0, 0, 0); ServerConfig::g_CronJobsTimer.schedule(new VerificationEmailResendTimerTask(user_id), Poco::Timestamp(runDateTime.timestamp())); count_scheduled++; scheduledResendTask |= 2; } //if(count_scheduled_at_once > ) } if (count_scheduled_at_once) printf("scheduled %d verification email resend at once\n", count_scheduled_at_once); if (count_scheduled) printf("scheduled %d verification email resend in the next 7 days\n", count_scheduled); } return 0; } int User::addMissingEmailHashes() { auto cm = ConnectionManager::getInstance(); auto em = ErrorManager::getInstance(); static const char* function_name = "User::addMissingEmailHashes"; auto session = cm->getConnection(CONNECTION_MYSQL_LOGIN_SERVER); Poco::Data::Statement select(session); std::vector> results; select << "select id, email from users " << "where email_hash IS NULL " , Poco::Data::Keywords::into(results) ; int result_count = 0; try { result_count = select.execute(); } catch (Poco::Exception& ex) { em->addError(new ParamError(function_name, "mysql error by select", ex.displayText().data())); em->sendErrorsAsEmail(); //return -1; } if (0 == result_count) return 0; std::vector> updates; // calculate hashes updates.reserve(results.size()); unsigned char email_hash[crypto_generichash_BYTES]; for (auto it = results.begin(); it != results.end(); it++) { memset(email_hash, 0, crypto_generichash_BYTES); auto id = it->get<0>(); auto email = it->get<1>(); crypto_generichash(email_hash, crypto_generichash_BYTES, (const unsigned char*)email.data(), email.size(), NULL, 0); updates.push_back(Poco::Tuple(Poco::Data::BLOB(email_hash, crypto_generichash_BYTES), id)); } // update db // reuse connection, I hope it's working Poco::Data::Statement update(session); update << "UPDATE users set email_hash = ? where id = ?" , Poco::Data::Keywords::use(updates); int updated_count = 0; try { updated_count = update.execute(); } catch (Poco::Exception& ex) { em->addError(new ParamError(function_name, "mysql error by update", ex.displayText().data())); em->sendErrorsAsEmail(); } return updated_count; } std::string User::getGroupBaseUrl() { UNIQUE_LOCK; static const char* function_name = "User::getGroupBaseUrl"; if (mGroupBaseUrl != "") { printf("[%s] return saved group base Url: %s\n", function_name, mGroupBaseUrl.data()); return mGroupBaseUrl; } auto model = getModel(); if (!model->getGroupId()) { printf("[%s] return ServerConfig::g_php_serverPath because no group id\n", function_name); return ServerConfig::g_php_serverPath; } auto group = controller::Group::load(model->getGroupId()); if (!group.isNull()) { auto group_model = group->getModel(); if (ServerConfig::g_ServerSetupType == ServerConfig::SERVER_TYPE_TEST) { mGroupBaseUrl = "http://" + group_model->getUrl() + group_model->getHome(); } else { mGroupBaseUrl = "https://" + group_model->getUrl() + group_model->getHome(); } printf("[%s] return group base Url: %s from Group\n", function_name, mGroupBaseUrl.data()); return mGroupBaseUrl; } return ServerConfig::g_php_serverPath; } Poco::AutoPtr User::getGroup() { auto model = getModel(); if (!model->getGroupId()) { return nullptr; } return controller::Group::load(model->getGroupId()); } }