diff --git a/vpn/openconnect/CMakeLists.txt b/vpn/openconnect/CMakeLists.txt --- a/vpn/openconnect/CMakeLists.txt +++ b/vpn/openconnect/CMakeLists.txt @@ -39,7 +39,7 @@ openconnectauthworkerthread.cpp ) - ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui) + ki18n_wrap_ui(openconnect_SRCS openconnectprop.ui openconnectauth.ui openconnecttoken.ui) add_library(plasmanetworkmanagement_openconnectui ${openconnect_SRCS}) diff --git a/vpn/openconnect/nm-openconnect-service.h b/vpn/openconnect/nm-openconnect-service.h --- a/vpn/openconnect/nm-openconnect-service.h +++ b/vpn/openconnect/nm-openconnect-service.h @@ -38,10 +38,13 @@ #define NM_OPENCONNECT_KEY_PRIVKEY "userkey" #define NM_OPENCONNECT_KEY_MTU "mtu" #define NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID "pem_passphrase_fsid" +#define NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT "prevent_invalid_cert" #define NM_OPENCONNECT_KEY_PROTOCOL "protocol" #define NM_OPENCONNECT_KEY_PROXY "proxy" #define NM_OPENCONNECT_KEY_CSD_ENABLE "enable_csd_trojan" #define NM_OPENCONNECT_KEY_CSD_WRAPPER "csd_wrapper" +#define NM_OPENCONNECT_KEY_TOKEN_MODE "stoken_source" +#define NM_OPENCONNECT_KEY_TOKEN_SECRET "stoken_string" #define NM_OPENCONNECT_USER "nm-openconnect" diff --git a/vpn/openconnect/openconnectauth.h b/vpn/openconnect/openconnectauth.h --- a/vpn/openconnect/openconnectauth.h +++ b/vpn/openconnect/openconnectauth.h @@ -55,13 +55,15 @@ void writeNewConfig(const QString &); void validatePeerCert(const QString &, const QString &, const QString &, bool*); void processAuthForm(struct oc_auth_form *); - void updateLog(const QString &, const int &); + void updateLog(const QString&, const int&); void logLevelChanged(int); void formLoginClicked(); void formGroupChanged(); void workerFinished(const int&); void viewServerLogToggled(bool); void connectHost(); + void initTokens(); + }; #endif // OPENCONNECTAUTH_H diff --git a/vpn/openconnect/openconnectauth.cpp b/vpn/openconnect/openconnectauth.cpp --- a/vpn/openconnect/openconnectauth.cpp +++ b/vpn/openconnect/openconnectauth.cpp @@ -57,6 +57,18 @@ #include } +#if !OPENCONNECT_CHECK_VER(2,1) +#define __openconnect_set_token_mode(...) -EOPNOTSUPP +#elif !OPENCONNECT_CHECK_VER(2,2) +#define __openconnect_set_token_mode(vpninfo, mode, secret) openconnect_set_stoken_mode(vpninfo, 1, secret) +#else +#define __openconnect_set_token_mode openconnect_set_token_mode +#endif + +#if OPENCONNECT_CHECK_VER(3,4) + static int updateToken(void*, const char*); +#endif + // name/address: IP/domain name of the host (OpenConnect accepts both, so no difference here) // group: user group on the server typedef struct { @@ -65,6 +77,11 @@ QString address; } VPNHost; +typedef struct { + oc_token_mode_t tokenMode; + QByteArray tokenSecret; +} Token; + class OpenconnectAuthWidgetPrivate { public: @@ -82,6 +99,8 @@ int cancelPipes[2]; QList > serverLog; int passwordFormIndex; + QByteArray tokenMode; + Token token; enum LogLevels {Error = 0, Info, Debug, Trace}; }; @@ -122,10 +141,15 @@ connect(d->worker, &OpenconnectAuthWorkerThread::updateLog, this, &OpenconnectAuthWidget::updateLog); connect(d->worker, QOverload::of(&OpenconnectAuthWorkerThread::writeNewConfig), this, &OpenconnectAuthWidget::writeNewConfig); connect(d->worker, &OpenconnectAuthWorkerThread::cookieObtained, this, &OpenconnectAuthWidget::workerFinished); + connect(d->worker, &OpenconnectAuthWorkerThread::initTokens, this, &OpenconnectAuthWidget::initTokens); readConfig(); readSecrets(); +#if OPENCONNECT_CHECK_VER(3,4) + openconnect_set_token_callbacks(d->vpninfo, &d->secrets, NULL, &updateToken); +#endif + // This might be set by readSecrets() so don't connect it until now connect(d->ui.cmbHosts, QOverload::of(&QComboBox::currentIndexChanged), this, &OpenconnectAuthWidget::connectHost); @@ -196,6 +220,9 @@ const QString protocol = dataMap[NM_OPENCONNECT_KEY_PROTOCOL]; openconnect_set_protocol(d->vpninfo, OC3DUP(protocol == "juniper" ? "nc" : protocol.toUtf8().data())); } + + d->tokenMode = dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE].toUtf8(); + } void OpenconnectAuthWidget::readSecrets() @@ -248,11 +275,42 @@ if (d->secrets["save_passwords"] == "yes") { d->ui.chkStorePasswords->setChecked(true); } + + QByteArray tokenSecret = d->secrets[NM_OPENCONNECT_KEY_TOKEN_SECRET].toUtf8(); + if (!d->tokenMode.isEmpty()) { + int ret = 0; + + if (d->tokenMode == "manual" && !tokenSecret.isEmpty()) + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, tokenSecret); + else if (d->tokenMode =="stokenrc") + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_STOKEN, NULL); + else if (d->tokenMode == "totp" && !tokenSecret.isEmpty()) + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_TOTP, tokenSecret); +#if OPENCONNECT_CHECK_VER(3,4) + else if (d->tokenMode == "hotp" && !tokenSecret.isEmpty()) + ret = __openconnect_set_token_mode(d->vpninfo, OC_TOKEN_MODE_HOTP, tokenSecret); +#endif +#if OPENCONNECT_CHECK_VER(5,0) + else if (d->tokenMode == "yubioath") { + /* This needs to be done from a thread because it can call back to + ask for the PIN */ + d->token.tokenMode = OC_TOKEN_MODE_YUBIOATH; + if (!tokenSecret.isEmpty() && tokenSecret.length()) + d->token.tokenSecret = tokenSecret; + else + d->token.tokenSecret = NULL; + } +#endif + if (ret) { + addFormInfo(QLatin1String("dialog-error"), i18n("Failed to initialize software token: %1\n", ret)); + } + } } void OpenconnectAuthWidget::acceptDialog() { // Find top-level widget as this should be the QDialog itself + updateLog(QLatin1String("acceptDialog"),PRG_INFO); QWidget *widget = parentWidget(); while (widget->parentWidget() != nullptr) { widget = widget->parentWidget(); @@ -269,6 +327,7 @@ void OpenconnectAuthWidget::connectHost() { Q_D(OpenconnectAuthWidget); + updateLog(QLatin1String("connectHost"),PRG_INFO); d->userQuit = true; if (write(d->cancelPipes[1], "x", 1)) { // not a lot we can do @@ -298,16 +357,27 @@ } d->secrets["lasthost"] = host.name; addFormInfo(QLatin1String("dialog-information"), i18n("Contacting host, please wait...")); + d->worker->start(); } +void OpenconnectAuthWidget::initTokens() +{ + Q_D(OpenconnectAuthWidget); + updateLog(QString("initTokens"),PRG_INFO); + if (d->token.tokenMode != OC_TOKEN_MODE_NONE) + __openconnect_set_token_mode(d->vpninfo, d->token.tokenMode, d->token.tokenSecret); +} + QVariantMap OpenconnectAuthWidget::setting() const { + Q_D(const OpenconnectAuthWidget); NMStringMap secrets; QVariantMap secretData; + secrets.unite(d->secrets); QString host(openconnect_get_hostname(d->vpninfo)); const QString port = QString::number(openconnect_get_port(d->vpninfo)); @@ -346,6 +416,15 @@ return secretData; } +#if OPENCONNECT_CHECK_VER(3,4) +static int updateToken(void *cbdata, const char *tok) +{ + NMStringMap *secrets = static_cast(cbdata); + secrets->insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), QLatin1String(tok)); + return 0; +} +#endif + void OpenconnectAuthWidget::writeNewConfig(const QString & buf) { Q_D(OpenconnectAuthWidget); @@ -401,6 +480,7 @@ void OpenconnectAuthWidget::addFormInfo(const QString &iconName, const QString &message) { Q_D(OpenconnectAuthWidget); + updateLog("addFormInfo",PRG_INFO); QHBoxLayout *layout = new QHBoxLayout(); QLabel *icon = new QLabel(this); QSizePolicy sizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); @@ -426,6 +506,7 @@ void OpenconnectAuthWidget::processAuthForm(struct oc_auth_form *form) { Q_D(OpenconnectAuthWidget); + updateLog("processAuthForm",PRG_INFO); deleteAllFromLayout(d->ui.loginBoxLayout); if (form->banner) { addFormInfo(QLatin1String("dialog-information"), form->banner); @@ -451,6 +532,7 @@ QWidget *widget = nullptr; const QString key = QString("form:%1:%2").arg(QLatin1String(form->auth_id)).arg(QLatin1String(opt->name)); const QString value = d->secrets.value(key); + updateLog(QString(opt->type),PRG_INFO); if (opt->type == OC_FORM_OPT_PASSWORD || opt->type == OC_FORM_OPT_TEXT) { PasswordField *le = new PasswordField(this); le->setText(value); @@ -571,6 +653,10 @@ connect(buttons, &QDialogButtonBox::rejected, dialog.data(), &QDialog::reject); dialog->layout()->addWidget(widget); dialog->layout()->addWidget(buttons); + + const NMStringMap dataMap = d->setting->data(); + buttons->button(QDialogButtonBox::Ok)->setEnabled(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] != "yes"); + if(dialog.data()->exec() == QDialog::Accepted) { *accepted = true; } else { @@ -594,6 +680,7 @@ void OpenconnectAuthWidget::formGroupChanged() { Q_D(OpenconnectAuthWidget); + updateLog("formGroupChanged",PRG_INFO); d->formGroupChanged = true; formLoginClicked(); @@ -605,6 +692,7 @@ void OpenconnectAuthWidget::formLoginClicked() { Q_D(OpenconnectAuthWidget); + updateLog("formLoginClicked",PRG_INFO); const int lastIndex = d->ui.loginBoxLayout->count() - 1; QLayout *layout = d->ui.loginBoxLayout->itemAt(d->passwordFormIndex)->layout(); @@ -623,6 +711,7 @@ if (opt->type == OC_FORM_OPT_TEXT) { d->secrets.insert(key, le->text()); } else { + updateLog(QString("formLoginClicked: insert")+key+": "+le->text(),PRG_INFO); d->tmpSecrets.insert(key, le->text()); } } else if (opt->type == OC_FORM_OPT_SELECT) { @@ -641,6 +730,7 @@ void OpenconnectAuthWidget::workerFinished(const int &ret) { Q_D(OpenconnectAuthWidget); + updateLog("workerFinished",PRG_INFO); if (ret < 0) { QString message; QList >::const_iterator i; diff --git a/vpn/openconnect/openconnectauthworkerthread.h b/vpn/openconnect/openconnectauthworkerthread.h --- a/vpn/openconnect/openconnectauthworkerthread.h +++ b/vpn/openconnect/openconnectauthworkerthread.h @@ -91,7 +91,8 @@ void updateLog(const QString &, const int&); void writeNewConfig(const QString &); void cookieObtained(const int&); - + void initTokens(void); + protected: void run() override; diff --git a/vpn/openconnect/openconnectauthworkerthread.cpp b/vpn/openconnect/openconnectauthworkerthread.cpp --- a/vpn/openconnect/openconnectauthworkerthread.cpp +++ b/vpn/openconnect/openconnectauthworkerthread.cpp @@ -116,6 +116,7 @@ void OpenconnectAuthWorkerThread::run() { openconnect_init_ssl(); + Q_EMIT initTokens(); int ret = openconnect_obtain_cookie(m_openconnectInfo); if (*m_userDecidedToQuit) { return; diff --git a/vpn/openconnect/openconnectprop.ui b/vpn/openconnect/openconnectprop.ui --- a/vpn/openconnect/openconnectprop.ui +++ b/vpn/openconnect/openconnectprop.ui @@ -6,14 +6,20 @@ 0 0 - 339 - 364 + 395 + 488 + + + 0 + 0 + + OpenConnect Settings - + @@ -94,6 +100,9 @@ VPN Protocol: + + cmbProtocol + @@ -125,9 +134,6 @@ Certificate Authentication - - 6 - @@ -138,6 +144,13 @@ + + + + *.pem *.crt *.key + + + @@ -148,13 +161,6 @@ - - - - *.pem *.crt *.key - - - @@ -169,6 +175,40 @@ + + + + Prevent the user from manually accepting +invalid certificates + + + + + + + + + + + + + Qt::Horizontal + + + + 278 + 20 + + + + + + + + Tokens + + + @@ -190,10 +230,13 @@ KUrlRequester - QWidget + QFrame
kurlrequester.h
+ + enableTokenSecret(int) + diff --git a/vpn/openconnect/openconnecttoken.ui b/vpn/openconnect/openconnecttoken.ui new file mode 100644 --- /dev/null +++ b/vpn/openconnect/openconnecttoken.ui @@ -0,0 +1,97 @@ + + + OpenConnectToken + + + + 0 + 0 + 394 + 191 + + + + Form + + + + + + Software Token Authentication + + + + + + Token Mode + + + cmbTokenMode + + + + + + + Token Secret: + + + leTokenSecret + + + + + + + false + + + false + + + + + + + QComboBox::AdjustToContents + + + + Disabled + + + + + RSA SecurID — read from ~/.stokenrc + + + + + RSA SecurID — manually entered + + + + + TOTP — manually entered + + + + + HOTP — manually entered + + + + + Yubikey OATH + + + + + + + + + + + + diff --git a/vpn/openconnect/openconnectwidget.h b/vpn/openconnect/openconnectwidget.h --- a/vpn/openconnect/openconnectwidget.h +++ b/vpn/openconnect/openconnectwidget.h @@ -38,7 +38,13 @@ void loadConfig(const NetworkManager::Setting::Ptr &setting) override; QVariantMap setting() const override; bool isValid() const override; + void enableTokenSecret(int index); + bool initTokenGroup(); + void loadSecrets(const NetworkManager::Setting::Ptr &setting) override; +private Q_SLOTS: + void showTokens(); + private: OpenconnectSettingWidgetPrivate *const d_ptr; }; diff --git a/vpn/openconnect/openconnectwidget.cpp b/vpn/openconnect/openconnectwidget.cpp --- a/vpn/openconnect/openconnectwidget.cpp +++ b/vpn/openconnect/openconnectwidget.cpp @@ -24,15 +24,38 @@ #include #include "ui_openconnectprop.h" +#include "ui_openconnecttoken.h" #include +#include #include "nm-openconnect-service.h" +#include +#ifndef OPENCONNECT_CHECK_VER +#define OPENCONNECT_CHECK_VER(x,y) 0 +#endif + +#if !OPENCONNECT_CHECK_VER(2,1) +#define openconnect_has_stoken_support() 0 +#endif +#if !OPENCONNECT_CHECK_VER(2,2) +#define openconnect_has_oath_support() 0 +#endif +#if !OPENCONNECT_CHECK_VER(5,0) +#define openconnect_has_yubioath_support() 0 +#endif + +#include + class OpenconnectSettingWidgetPrivate { public: Ui_OpenconnectProp ui; + Ui::OpenConnectToken tokenUi; NetworkManager::VpnSetting::Ptr setting; + mutable QStringList tokenModeList; + QDialog *tokenDlg; + QWidget *tokenWid; }; OpenconnectSettingWidget::OpenconnectSettingWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget * parent) @@ -42,13 +65,32 @@ Q_D(OpenconnectSettingWidget); d->ui.setupUi(this); d->setting = setting; + + d->tokenModeList = QStringList() << "disabled" << "stokenrc" << "manual" << "totp" << "hotp" << "yubioath"; // Connect for setting check watchChangedSetting(); // Connect for validity check connect(d->ui.leGateway, &QLineEdit::textChanged, this, &OpenconnectSettingWidget::slotWidgetChanged); - + connect(d->ui.buTokens, &QPushButton::clicked, this, &OpenconnectSettingWidget::showTokens); + + + d->tokenDlg = new QDialog(this); + d->tokenWid = new QWidget(this); + d->tokenUi.setupUi(d->tokenWid); + QVBoxLayout * layout = new QVBoxLayout(d->tokenDlg); + layout->addWidget(d->tokenWid); + d->tokenDlg->setLayout(layout); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, d->tokenDlg); + connect(buttons, &QDialogButtonBox::accepted, d->tokenDlg, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, d->tokenDlg, &QDialog::reject); + layout->addWidget(buttons); + + connect(d->tokenUi.cmbTokenMode, QOverload::of(&QComboBox::currentIndexChanged), this, QOverload::of((&OpenconnectSettingWidget::enableTokenSecret))); + connect(d->tokenUi.leTokenSecret, &QPlainTextEdit::textChanged, this, &OpenconnectSettingWidget::slotWidgetChanged); + d->tokenUi.gbToken->setVisible(initTokenGroup()); + KAcceleratorManager::manage(this); if (d->setting) { @@ -61,13 +103,51 @@ delete d_ptr; } +void OpenconnectSettingWidget::enableTokenSecret(int index) +{ + Q_D(const OpenconnectSettingWidget); + + d->tokenUi.leTokenSecret->setEnabled(index > 1); +} + +bool OpenconnectSettingWidget::initTokenGroup() +{ + Q_D(const OpenconnectSettingWidget); + int validRows = 0; + + // iterate through the items and find the ones to be removed + QComboBox *combo = d->tokenUi.cmbTokenMode; + + for (int i = combo->count()-1; i>=0 ; --i) { + if (combo->itemText(i).startsWith("RSA") && !openconnect_has_stoken_support ()) { + combo->removeItem(i); + d->tokenModeList.removeAt(i); + } + else if (combo->itemText(i).startsWith("TOTP") && !openconnect_has_oath_support ()) { + combo->removeItem(i); + d->tokenModeList.removeAt(i); + } + else if (combo->itemText(i).startsWith("HOTP") && !openconnect_has_oath_support ()) { + combo->removeItem(i); + d->tokenModeList.removeAt(i); + } + else if (combo->itemText(i).startsWith("Yubikey") && !openconnect_has_yubioath_support ()) { + combo->removeItem(i); + d->tokenModeList.removeAt(i); + } + else + ++validRows; + } + return validRows > 1; +} + void OpenconnectSettingWidget::loadConfig(const NetworkManager::Setting::Ptr &setting) { Q_D(OpenconnectSettingWidget); - Q_UNUSED(setting) // General settings const NMStringMap dataMap = d->setting->data(); + d->ui.cmbProtocol->setCurrentIndex(dataMap[NM_OPENCONNECT_KEY_PROTOCOL] != QLatin1String("anyconnect")); d->ui.leGateway->setText(dataMap[NM_OPENCONNECT_KEY_GATEWAY]); @@ -78,6 +158,27 @@ d->ui.leUserCert->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_USERCERT])); d->ui.leUserPrivateKey->setUrl(QUrl::fromLocalFile(dataMap[NM_OPENCONNECT_KEY_PRIVKEY])); d->ui.chkUseFsid->setChecked(dataMap[NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID] == "yes"); + d->ui.preventInvalidCert->setChecked(dataMap[NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT] == "yes"); + + int index = d->tokenModeList.indexOf(dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE]); + if (index > 0) { + d->tokenUi.cmbTokenMode->setCurrentIndex(index); + if (index > 1) { + loadSecrets(setting); + } + } +} + +void OpenconnectSettingWidget::loadSecrets(const NetworkManager::Setting::Ptr &setting) +{ + Q_D(OpenconnectSettingWidget); + + NetworkManager::VpnSetting::Ptr vpnSetting = setting.staticCast(); + + if (vpnSetting) { + const NMStringMap secrets = vpnSetting->secrets(); + d->tokenUi.leTokenSecret->appendPlainText(secrets.value(NM_OPENCONNECT_KEY_TOKEN_SECRET)); + } } QVariantMap OpenconnectSettingWidget::setting() const @@ -88,6 +189,7 @@ setting.setServiceType(QLatin1String(NM_DBUS_SERVICE_OPENCONNECT)); NMStringMap data; + NMStringMap secrets; data.insert(NM_OPENCONNECT_KEY_PROTOCOL, d->ui.cmbProtocol->currentIndex() ? QLatin1String("nc") : QLatin1String("anyconnect")); data.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY), d->ui.leGateway->text()); @@ -108,7 +210,11 @@ data.insert(QLatin1String(NM_OPENCONNECT_KEY_PRIVKEY), d->ui.leUserPrivateKey->url().toLocalFile()); } data.insert(QLatin1String(NM_OPENCONNECT_KEY_PEM_PASSPHRASE_FSID), d->ui.chkUseFsid->isChecked() ? "yes" : "no"); + data.insert(QLatin1String(NM_OPENCONNECT_KEY_PREVENT_INVALID_CERT), d->ui.preventInvalidCert->isChecked() ? "yes" : "no"); + data.insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_MODE), d->tokenModeList[d->tokenUi.cmbTokenMode->currentIndex()]); + secrets.insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_SECRET), d->tokenUi.leTokenSecret->toPlainText()); + // Restore previous flags, this is necessary for keeping secrets stored in KWallet Q_FOREACH (const QString &key, d->setting->data().keys()) { if (key.contains(QLatin1String("-flags"))) { @@ -122,11 +228,17 @@ data.insert(QLatin1String(NM_OPENCONNECT_KEY_GATEWAY"-flags"), QString::number(NetworkManager::Setting::NotSaved)); setting.setData(data); - setting.setSecrets(d->setting->secrets()); + setting.setSecrets(secrets); return setting.toMap(); } +void OpenconnectSettingWidget::showTokens() +{ + Q_D(OpenconnectSettingWidget); + + d->tokenDlg->show(); +} bool OpenconnectSettingWidget::isValid() const { Q_D(const OpenconnectSettingWidget);