diff --git a/client/Application.cpp b/client/Application.cpp index 66b4107d5..75e9fa43a 100644 --- a/client/Application.cpp +++ b/client/Application.cpp @@ -98,6 +98,20 @@ class DigidocConf final: public digidoc::XmlConfCurrent enableLog(Settings::LIBDIGIDOCPP_DEBUG); Settings::LIBDIGIDOCPP_DEBUG = false; Settings::LIBDIGIDOCPP_DEBUG.registerCallback([this](const bool &value) { enableLog(value); }); +#ifdef Q_OS_MACOS + if(Settings::PROXY_HOST.isSet() || Settings::PROXY_PORT.isSet() + || Settings::PROXY_USER.isSet() || Settings::PROXY_PASS.isSet()) + { + if(Application::setProxyCredentials({Settings::PROXY_HOST, Settings::PROXY_PORT, + Settings::PROXY_USER, Settings::PROXY_PASS})) + { + Settings::PROXY_HOST.clear(); + Settings::PROXY_PORT.clear(); + Settings::PROXY_USER.clear(); + Settings::PROXY_PASS.clear(); + } + } +#endif #ifndef Q_OS_DARWIN setTSLOnlineDigest(true); #endif @@ -123,24 +137,44 @@ class DigidocConf final: public digidoc::XmlConfCurrent std::string proxyHost() const final { +#ifdef Q_OS_MACOS + if(Settings::PROXY_CONFIG == Settings::ProxyManual) + if(const auto credentials = Application::proxyCredentials()) + return credentials->host.toStdString(); +#endif return proxyConf(&QNetworkProxy::hostName, Settings::PROXY_HOST, [this] { return digidoc::XmlConfCurrent::proxyHost(); }); } std::string proxyPort() const final { +#ifdef Q_OS_MACOS + if(Settings::PROXY_CONFIG == Settings::ProxyManual) + if(const auto credentials = Application::proxyCredentials()) + return credentials->port.toStdString(); +#endif return proxyConf([](const QNetworkProxy &systemProxy) { return QString::number(systemProxy.port()); }, Settings::PROXY_PORT, [this] { return digidoc::XmlConfCurrent::proxyPort(); }); } std::string proxyUser() const final { +#ifdef Q_OS_MACOS + if(Settings::PROXY_CONFIG == Settings::ProxyManual) + if(const auto credentials = Application::proxyCredentials()) + return credentials->user.toStdString(); +#endif return proxyConf(&QNetworkProxy::user, Settings::PROXY_USER, [this] { return digidoc::XmlConfCurrent::proxyUser(); }); } std::string proxyPass() const final { +#ifdef Q_OS_MACOS + if(Settings::PROXY_CONFIG == Settings::ProxyManual) + if(const auto credentials = Application::proxyCredentials()) + return credentials->password.toStdString(); +#endif return proxyConf(&QNetworkProxy::password, Settings::PROXY_PASS, [this] { return digidoc::XmlConfCurrent::proxyPass(); }); } diff --git a/client/Application.h b/client/Application.h index c458af40a..55544ff88 100644 --- a/client/Application.h +++ b/client/Application.h @@ -19,6 +19,8 @@ #pragma once +#include + #include #ifdef Q_OS_MAC @@ -76,7 +78,24 @@ class Application final: public BaseApplication static void showClient(QStringList params = {}, bool crypto = false, bool sign = false, bool newWindow = false); static void updateTSLCache(const QDateTime &tslTime); #if defined(Q_OS_MAC) + struct ProxyCredentials + { + ProxyCredentials(QString host, QString port, QString user, QString password); + ProxyCredentials(ProxyCredentials &&) noexcept; + ~ProxyCredentials(); + + Q_DISABLE_COPY(ProxyCredentials) + ProxyCredentials &operator=(ProxyCredentials &&) = delete; + + QString host; + QString port; + QString user; + QString password; + }; + static QString groupContainerPath(); + static std::optional proxyCredentials(); + static bool setProxyCredentials(ProxyCredentials credentials); #endif private Q_SLOTS: diff --git a/client/Application_mac.mm b/client/Application_mac.mm index 1282fdb97..7091337a3 100644 --- a/client/Application_mac.mm +++ b/client/Application_mac.mm @@ -19,13 +19,24 @@ #include "Application.h" -#include +#import +#include #include #include #include using namespace Qt::StringLiterals; +static NSMutableDictionary *proxyCredentialsQuery() +{ + return [@{ + (__bridge id)kSecClass: (__bridge id)kSecClassInternetPassword, + (__bridge id)kSecAttrProtocol: (__bridge id)kSecAttrProtocolHTTPProxy, + (__bridge id)kSecAttrSecurityDomain: + QStringLiteral("%1.proxy").arg(QGuiApplication::desktopFileName()).toNSString() + } mutableCopy]; +} + static auto fetchPaths(NSPasteboard *pboard) { QStringList result; @@ -116,3 +127,86 @@ - (void)openCrypto:(NSPasteboard *)pboard userData:(NSString *)data error:(NSStr containerURLForSecurityApplicationGroupIdentifier:@"group.ee.ria.qdigidoc4.tsl"].path); } +Application::ProxyCredentials::ProxyCredentials( + QString host, QString port, QString user, QString password) + : host(std::move(host)) + , port(std::move(port)) + , user(std::move(user)) + , password(std::move(password)) +{} + +Application::ProxyCredentials::ProxyCredentials(ProxyCredentials &&) noexcept = default; + +Application::ProxyCredentials::~ProxyCredentials() +{ + password.fill(QChar{}); +} + +std::optional Application::proxyCredentials() +{ + NSMutableDictionary *query = proxyCredentialsQuery(); + query[(__bridge id)kSecReturnAttributes] = @YES; + query[(__bridge id)kSecReturnData] = @YES; + query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; + + CFTypeRef result = nullptr; + const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); + if(status == errSecItemNotFound) + return std::nullopt; + if(status != errSecSuccess) + { + qWarning() << "Failed to read the proxy credentials from Keychain:" << status; + return std::nullopt; + } + + NSDictionary *item = CFBridgingRelease(result); + NSData *data = item[(__bridge id)kSecValueData]; + const quint16 port = [item[(__bridge id)kSecAttrPort] unsignedShortValue]; + return ProxyCredentials { + QString::fromNSString(item[(__bridge id)kSecAttrServer]), + port ? QString::number(port) : QString(), + QString::fromNSString(item[(__bridge id)kSecAttrAccount]), + QString::fromUtf8(static_cast(data.bytes), qsizetype(data.length)) + }; +} + +bool Application::setProxyCredentials(ProxyCredentials credentials) +{ + NSMutableDictionary *query = proxyCredentialsQuery(); + if(credentials.host.isEmpty() && credentials.port.isEmpty() && + credentials.user.isEmpty() && credentials.password.isEmpty()) + { + const OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + if(status == errSecSuccess || status == errSecItemNotFound) + return true; + qWarning() << "Failed to remove the proxy credentials from Keychain:" << status; + return false; + } + + QByteArray utf8 = credentials.password.toUtf8(); + auto utf8Scope = qScopeGuard([&] { utf8.fill(0); }); + NSData *data = [NSData dataWithBytesNoCopy:utf8.data() + length:NSUInteger(utf8.size()) freeWhenDone:NO]; + NSDictionary *attributes = @{ + (__bridge id)kSecAttrServer: credentials.host.toNSString(), + (__bridge id)kSecAttrPort: @(credentials.port.toUShort()), + (__bridge id)kSecAttrAccount: credentials.user.toNSString(), + (__bridge id)kSecValueData: data + }; + + // SecItemUpdate keeps the stored password when the new value is empty, + // recreate the item to clear it + OSStatus status = utf8.isEmpty() ? errSecItemNotFound : + SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)attributes); + if(status == errSecItemNotFound) + { + SecItemDelete((__bridge CFDictionaryRef)query); + [query addEntriesFromDictionary:attributes]; + status = SecItemAdd((__bridge CFDictionaryRef)query, nullptr); + } + + if(status == errSecSuccess) + return true; + qWarning() << "Failed to store the proxy credentials in Keychain:" << status; + return false; +} diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 475f57341..0b87c1b7c 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -138,7 +138,7 @@ if( APPLE ) set_source_files_properties(${RESOURCE_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties( Application_mac.mm dialogs/CertificateDetails_mac.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc" ) set_source_files_properties( LdapSearch.cpp PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations" ) - target_link_libraries(${PROJECT_NAME} "-framework QuickLookUI" "-fobjc-arc") + target_link_libraries(${PROJECT_NAME} "-framework QuickLookUI" "-framework Security" "-fobjc-arc") find_library(PKCS11_MODULE NAMES opensc-pkcs11.so HINTS /Library/OpenSC/lib) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND cp -a ${PKCS11_MODULE} $ diff --git a/client/dialogs/SettingsDialog.cpp b/client/dialogs/SettingsDialog.cpp index 9ad9e6a4f..e4a6710aa 100644 --- a/client/dialogs/SettingsDialog.cpp +++ b/client/dialogs/SettingsDialog.cpp @@ -57,6 +57,37 @@ using namespace Qt::StringLiterals; #define qdigidoc4log QStringLiteral("%1/%2.log").arg(QDir::tempPath(), QApplication::applicationName()) +namespace +{ + +void applyProxyConfig(int config, const QString &host, const QString &port, + const QString &user, const QString &pass) +{ + switch(config) + { + case Settings::ProxyNone: + QNetworkProxyFactory::setUseSystemConfiguration(false); + QNetworkProxy::setApplicationProxy({}); + break; + case Settings::ProxySystem: + QNetworkProxyFactory::setUseSystemConfiguration(true); + break; + default: + QNetworkProxyFactory::setUseSystemConfiguration(false); + // QAuthenticator encodes Basic credentials with toLatin1() while proxies and + // libdigidocpp's Connect::sendProxyAuth() send them as UTF-8. Pre-encode the + // credentials so that toLatin1() restores the UTF-8 bytes; the conversion is + // a no-op for US-ASCII credentials. Only Basic is usable anyway, libdigidocpp + // implements no other proxy authentication scheme. + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, + host, port.toUShort(), + QString::fromLatin1(user.toUtf8()), QString::fromLatin1(pass.toUtf8()))); + break; + } +} + +} + SettingsDialog::SettingsDialog(int page, QWidget *parent) : QDialog(parent) , ui(new Ui::SettingsDialog) @@ -263,7 +294,12 @@ SettingsDialog::SettingsDialog(int page, QWidget *parent) updateCDoc2Cert(QSslCertificate(QByteArray::fromBase64(Settings::CDOC2_GET_CERT), QSsl::Der)); // pageProxy +#ifdef Q_OS_MACOS + connect(this, &SettingsDialog::accepted, this, &SettingsDialog::saveProxy); + connect(this, &SettingsDialog::rejected, this, [] { loadProxy(digidoc::Conf::instance()); }); +#else connect(this, &SettingsDialog::finished, this, &SettingsDialog::saveProxy); +#endif ui->proxyGroup->setId(ui->rdProxyNone, Settings::ProxyNone); ui->proxyGroup->setId(ui->rdProxySystem, Settings::ProxySystem); ui->proxyGroup->setId(ui->rdProxyManual, Settings::ProxyManual); @@ -271,14 +307,20 @@ SettingsDialog::SettingsDialog(int page, QWidget *parent) connect(ui->rdProxyManual, &QRadioButton::toggled, ui->wgtProxyManual, &QWidget::setVisible); ui->proxyGroup->button(Settings::PROXY_CONFIG)->setChecked(true); #ifdef Q_OS_MACOS - ui->txtProxyHost->setText(Settings::PROXY_HOST); - ui->txtProxyPort->setText(Settings::PROXY_PORT); - ui->txtProxyUsername->setText(Settings::PROXY_USER); - ui->txtProxyPassword->setText(Settings::PROXY_PASS); - connect(ui->txtProxyHost, &QLineEdit::textChanged, this, Settings::PROXY_HOST); - connect(ui->txtProxyPort, &QLineEdit::textChanged, this, Settings::PROXY_PORT); - connect(ui->txtProxyUsername, &QLineEdit::textChanged, this, Settings::PROXY_USER); - connect(ui->txtProxyPassword, &QLineEdit::textChanged, this, Settings::PROXY_PASS); + if(const auto credentials = Application::proxyCredentials()) + { + ui->txtProxyHost->setText(credentials->host); + ui->txtProxyPort->setText(credentials->port); + ui->txtProxyUsername->setText(credentials->user); + ui->txtProxyPassword->setText(credentials->password); + } + else + { + ui->txtProxyHost->setText(Settings::PROXY_HOST); + ui->txtProxyPort->setText(Settings::PROXY_PORT); + ui->txtProxyUsername->setText(Settings::PROXY_USER); + ui->txtProxyPassword->setText(Settings::PROXY_PASS); + } #else if(auto *i = digidoc::XmlConfCurrent::instance()) { @@ -431,7 +473,11 @@ QString SettingsDialog::certInfo(const SslCertificate &c) void SettingsDialog::checkConnection() { QApplication::setOverrideCursor( Qt::WaitCursor ); +#ifdef Q_OS_MACOS + applyProxy(); +#else saveProxy(); +#endif if(CheckConnection connection; !connection.check()) { Application::restoreOverrideCursor(); @@ -508,7 +554,16 @@ void SettingsDialog::selectLanguage() void SettingsDialog::saveProxy() { Settings::PROXY_CONFIG = ui->proxyGroup->checkedId(); -#ifndef Q_OS_MACOS +#ifdef Q_OS_MACOS + if(Application::setProxyCredentials({ui->txtProxyHost->text(), ui->txtProxyPort->text(), + ui->txtProxyUsername->text(), ui->txtProxyPassword->text()})) + { + Settings::PROXY_HOST.clear(); + Settings::PROXY_PORT.clear(); + Settings::PROXY_USER.clear(); + Settings::PROXY_PASS.clear(); + } +#else if(auto *i = digidoc::XmlConfCurrent::instance()) { i->setProxyHost(ui->txtProxyHost->text().toStdString()); @@ -520,26 +575,19 @@ void SettingsDialog::saveProxy() loadProxy(digidoc::Conf::instance()); } +void SettingsDialog::applyProxy() const +{ + applyProxyConfig(ui->proxyGroup->checkedId(), ui->txtProxyHost->text(), + ui->txtProxyPort->text(), ui->txtProxyUsername->text(), ui->txtProxyPassword->text()); +} + void SettingsDialog::loadProxy( const digidoc::Conf *conf ) { - switch(Settings::PROXY_CONFIG) - { - case Settings::ProxyNone: - QNetworkProxyFactory::setUseSystemConfiguration(false); - QNetworkProxy::setApplicationProxy({}); - break; - case Settings::ProxySystem: - QNetworkProxyFactory::setUseSystemConfiguration(true); - break; - default: - QNetworkProxyFactory::setUseSystemConfiguration(false); - QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, - QString::fromStdString(conf->proxyHost()), - QString::fromStdString(conf->proxyPort()).toUShort(), - QString::fromStdString(conf->proxyUser()), - QString::fromStdString(conf->proxyPass()))); - break; - } + applyProxyConfig(Settings::PROXY_CONFIG, + QString::fromStdString(conf->proxyHost()), + QString::fromStdString(conf->proxyPort()), + QString::fromStdString(conf->proxyUser()), + QString::fromStdString(conf->proxyPass())); } void SettingsDialog::updateDiagnostics() diff --git a/client/dialogs/SettingsDialog.h b/client/dialogs/SettingsDialog.h index beb558d15..c9ed74ff9 100644 --- a/client/dialogs/SettingsDialog.h +++ b/client/dialogs/SettingsDialog.h @@ -63,6 +63,7 @@ class SettingsDialog final: public QDialog private: void checkConnection(); + void applyProxy() const; void retranslate(const QString& lang); void saveFile(const QString &name, const QString &path); void saveFile(const QString &name, const QByteArray &content);