Index: vpn/openconnect/CMakeLists.txt =================================================================== --- vpn/openconnect/CMakeLists.txt +++ 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}) Index: vpn/openconnect/nm-openconnect-service.h =================================================================== --- vpn/openconnect/nm-openconnect-service.h +++ 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" Index: vpn/openconnect/openconnectauth.h =================================================================== --- vpn/openconnect/openconnectauth.h +++ vpn/openconnect/openconnectauth.h @@ -62,6 +62,7 @@ void workerFinished(const int&); void viewServerLogToggled(bool); void connectHost(); + void initTokens(); }; #endif // OPENCONNECTAUTH_H Index: vpn/openconnect/openconnectauth.cpp =================================================================== --- vpn/openconnect/openconnectauth.cpp +++ 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,8 @@ 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 +274,48 @@ if (d->secrets["save_passwords"] == "yes") { d->ui.chkStorePasswords->setChecked(true); } + + if (!d->tokenMode.isEmpty()) { + int ret = 0; + QByteArray tokenSecret = d->secrets[NM_OPENCONNECT_KEY_TOKEN_SECRET].toUtf8(); + + 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()) { + d->token.tokenSecret = tokenSecret; + } + else { + d->token.tokenSecret.clear(); + } + } +#endif + if (ret) { + addFormInfo(QLatin1String("dialog-error"), i18n("Failed to initialize software token: %1", 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 +332,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 @@ -301,6 +365,15 @@ 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); @@ -346,6 +419,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 +483,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 +509,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 +535,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::number(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 +656,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 +683,7 @@ void OpenconnectAuthWidget::formGroupChanged() { Q_D(OpenconnectAuthWidget); + updateLog("formGroupChanged",PRG_INFO); d->formGroupChanged = true; formLoginClicked(); @@ -605,6 +695,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 +714,7 @@ if (opt->type == OC_FORM_OPT_TEXT) { d->secrets.insert(key, le->text()); } else { + updateLog(QString("formLoginClicked: inserting ")+key,PRG_INFO); d->tmpSecrets.insert(key, le->text()); } } else if (opt->type == OC_FORM_OPT_SELECT) { @@ -641,6 +733,7 @@ void OpenconnectAuthWidget::workerFinished(const int &ret) { Q_D(OpenconnectAuthWidget); + updateLog("workerFinished",PRG_INFO); if (ret < 0) { QString message; QList >::const_iterator i; Index: vpn/openconnect/openconnectauthworkerthread.h =================================================================== --- vpn/openconnect/openconnectauthworkerthread.h +++ vpn/openconnect/openconnectauthworkerthread.h @@ -91,7 +91,7 @@ void updateLog(const QString &, const int&); void writeNewConfig(const QString &); void cookieObtained(const int&); - + void initTokens(void); protected: void run() override; Index: vpn/openconnect/openconnectauthworkerthread.cpp =================================================================== --- vpn/openconnect/openconnectauthworkerthread.cpp +++ 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; Index: vpn/openconnect/openconnectprop.ui =================================================================== --- vpn/openconnect/openconnectprop.ui +++ 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,39 @@ + + + + Invalid certificates won't be acepted + + + + + + + + + + + + + Qt::Horizontal + + + + 278 + 20 + + + + + + + + Show Tokens + + + @@ -190,10 +229,13 @@ KUrlRequester - QWidget + QFrame
kurlrequester.h
+ + enableTokenSecret(int) + Index: vpn/openconnect/openconnecttoken.ui =================================================================== --- /dev/null +++ vpn/openconnect/openconnecttoken.ui @@ -0,0 +1,74 @@ + + + OpenConnectToken + + + + 0 + 0 + 394 + 229 + + + + OpenConnect OTP Tokens + + + + + + Software Token Authentication + + + + + + Token Mode + + + cmbTokenMode + + + + + + + Token Secret: + + + leTokenSecret + + + + + + + QComboBox::AdjustToContents + + + + + + + false + + + false + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + Index: vpn/openconnect/openconnectwidget.h =================================================================== --- vpn/openconnect/openconnectwidget.h +++ vpn/openconnect/openconnectwidget.h @@ -38,6 +38,12 @@ 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; Index: vpn/openconnect/openconnectwidget.cpp =================================================================== --- vpn/openconnect/openconnectwidget.cpp +++ vpn/openconnect/openconnectwidget.cpp @@ -22,17 +22,37 @@ #include "openconnectwidget.h" #include #include +#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 + class OpenconnectSettingWidgetPrivate { public: Ui_OpenconnectProp ui; + Ui::OpenConnectToken tokenUi; NetworkManager::VpnSetting::Ptr setting; + QDialog *tokenDlg; }; OpenconnectSettingWidget::OpenconnectSettingWidget(const NetworkManager::VpnSetting::Ptr &setting, QWidget * parent) @@ -48,6 +68,19 @@ // 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->tokenUi.setupUi(d->tokenDlg); + QVBoxLayout * layout = new QVBoxLayout(d->tokenDlg); + layout->addWidget(d->tokenDlg); + d->tokenDlg->setLayout(layout); + connect(d->tokenUi.buttonBox, &QDialogButtonBox::accepted, d->tokenDlg, &QDialog::accept); + connect(d->tokenUi.buttonBox, &QDialogButtonBox::rejected, d->tokenDlg, &QDialog::reject); + + 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); @@ -61,10 +94,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; + QStringList tokenLabelList = QStringList() << "Disabled" << "RSA SecurID — read from ~/.stokenrc" << "RSA SecurID — manually entered" << "TOTP — manually entered" << "HOTP — manually entered" << "Yubikey"; + QStringList tokenModeList = QStringList() << "disabled" << "stokenrc" << "manual" << "totp" << "hotp" << "yubioath"; + QComboBox *combo = d->tokenUi.cmbTokenMode; + + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + if (openconnect_has_stoken_support ()) { + for ( ; validRows < 3; validRows++) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + } + } + if (openconnect_has_oath_support ()) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + if (OPENCONNECT_CHECK_VER(3,4)) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + validRows++; + } + } + if (openconnect_has_yubioath_support ()) { + combo->addItem(tokenLabelList[validRows]); + combo->setItemData(validRows, tokenModeList[validRows], Qt::UserRole); + } + return validRows > 0; +} + void OpenconnectSettingWidget::loadConfig(const NetworkManager::Setting::Ptr &setting) { Q_D(OpenconnectSettingWidget); - Q_UNUSED(setting) // General settings const NMStringMap dataMap = d->setting->data(); @@ -78,6 +152,29 @@ 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"); + + for (int index = 0; index < d->tokenUi.cmbTokenMode->count(); index++) { + if (d->tokenUi.cmbTokenMode->itemData(index, Qt::UserRole) == dataMap[NM_OPENCONNECT_KEY_TOKEN_MODE]) { + d->tokenUi.cmbTokenMode->setCurrentIndex(index); + if (index > 1) { + loadSecrets(setting); + } + break; + } + } +} + +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 +185,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,6 +206,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"); + + int index = d->tokenUi.cmbTokenMode->currentIndex(); + data.insert(QLatin1String(NM_OPENCONNECT_KEY_TOKEN_MODE), d->tokenUi.cmbTokenMode->itemData(index, Qt::UserRole).toString()); + 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()) { @@ -122,13 +225,20 @@ 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); + return !d->ui.leGateway->text().isEmpty(); }