diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d0b79147..d22ad7b88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,6 @@ project(kdeconnect VERSION ${RELEASE_SERVICE_VERSION}) set(KF_MIN_VERSION "5.101.0") set(QT_MIN_VERSION "5.15.2") -set(QCA_MIN_VERSION "2.1.0") find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) @@ -58,9 +57,6 @@ ecm_set_disabled_deprecation_versions( add_library(kdeconnectversion INTERFACE) target_include_directories(kdeconnectversion INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) -find_package(Qca-qt${QT_MAJOR_VERSION} ${QCA_MIN_VERSION} REQUIRED) -set(Qca_LIBRARY qca-qt${QT_MAJOR_VERSION}) - set(KF5_REQUIRED_COMPONENTS I18n ConfigWidgets DBusAddons IconThemes Notifications KIO KCMUtils Service Solid Kirigami2 People WindowSystem GuiAddons DocTools) set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index aef310736..6ab3953b4 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -42,6 +42,7 @@ target_sources(kdeconnectcore PRIVATE compositefiletransferjob.cpp daemon.cpp device.cpp + sslhelper.cpp core_debug.cpp notificationserverinfo.cpp openconfig.cpp @@ -55,13 +56,16 @@ ecm_qt_declare_logging_category(kdeconnectcore target_include_directories(kdeconnectcore PUBLIC ${PROJECT_SOURCE_DIR}) +find_package(OpenSSL REQUIRED) + target_link_libraries(kdeconnectcore PUBLIC Qt::Network KF${QT_MAJOR_VERSION}::CoreAddons - ${Qca_LIBRARY} KF${QT_MAJOR_VERSION}::KIOCore KF${QT_MAJOR_VERSION}::KIOGui + OpenSSL::Crypto + OpenSSL::SSL PRIVATE Qt::DBus KF${QT_MAJOR_VERSION}::I18n diff --git a/core/kdeconnectconfig.cpp b/core/kdeconnectconfig.cpp index f6153b018..217e56192 100644 --- a/core/kdeconnectconfig.cpp +++ b/core/kdeconnectconfig.cpp @@ -19,23 +19,19 @@ #include #include #include -#include #include "core_debug.h" #include "daemon.h" #include "dbushelper.h" #include "deviceinfo.h" #include "pluginloader.h" +#include "sslhelper.h" const QFile::Permissions strictPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser; struct KdeConnectConfigPrivate { - // The Initializer object sets things up, and also does cleanup when it goes out of scope - // Note it's not being used anywhere. That's intended - QCA::Initializer m_qcaInitializer; - - QCA::PrivateKey m_privateKey; - QSslCertificate m_certificate; // Use QSslCertificate instead of QCA::Certificate due to compatibility with QSslSocket + EVP_PKEY *m_privateKey; + QSslCertificate m_certificate; QSettings *m_config; QSettings *m_trustedDevices; @@ -59,15 +55,6 @@ KdeConnectConfig &KdeConnectConfig::instance() KdeConnectConfig::KdeConnectConfig() : d(new KdeConnectConfigPrivate) { - // qCDebug(KDECONNECT_CORE) << "QCA supported capabilities:" << QCA::supportedFeatures().join(","); - if (!QCA::isSupported("rsa")) { - qCritical() << "Could not find support for RSA in your QCA installation"; - Daemon::instance()->reportError(i18n("KDE Connect failed to start"), - i18n("Could not find support for RSA in your QCA installation. If your " - "distribution provides separate packets for QCA-ossl and QCA-gnupg, " - "make sure you have them installed and try again.")); - } - // Make sure base directory exists QDir().mkpath(baseConfigDir().path()); @@ -75,8 +62,7 @@ KdeConnectConfig::KdeConnectConfig() d->m_config = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("config")), QSettings::IniFormat); d->m_trustedDevices = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("trusted_devices")), QSettings::IniFormat); - loadPrivateKey(); - loadCertificate(); + loadOrGeneratePrivateKeyAndCertificate(privateKeyPath(), certificatePath()); if (name().isEmpty()) { setName(getDefaultDeviceName()); @@ -260,56 +246,48 @@ QDir KdeConnectConfig::pluginConfigDir(const QString &deviceId, const QString &p return QDir(pluginConfigDir); } -void KdeConnectConfig::loadPrivateKey() +bool KdeConnectConfig::loadPrivateKey(const QString &keyPath) { - QString keyPath = privateKeyPath(); QFile privKey(keyPath); - - bool needsToGenerateKey = false; if (privKey.exists() && privKey.open(QIODevice::ReadOnly)) { - QCA::ConvertResult result; - d->m_privateKey = QCA::PrivateKey::fromPEM(QString::fromLatin1(privKey.readAll()), QCA::SecureArray(), &result); - if (result != QCA::ConvertResult::ConvertGood) { - qCWarning(KDECONNECT_CORE) << "Private key from" << keyPath << "is not valid"; - needsToGenerateKey = true; + d->m_privateKey = SslHelper::pemToRsaPrivateKey(privKey.readAll()); + if (d->m_privateKey == nullptr) { + qCWarning(KDECONNECT_CORE) << "Private key from" << keyPath << "is not valid!"; } - } else { - needsToGenerateKey = true; } + return (d->m_privateKey == nullptr); +} + +bool KdeConnectConfig::loadCertificate(const QString &certPath) +{ + QFile cert(certPath); + if (cert.exists() && cert.open(QIODevice::ReadOnly)) { + auto loadedCerts = QSslCertificate::fromData(cert.readAll()); + if (loadedCerts.empty()) { + qCWarning(KDECONNECT_CORE) << "Certificate from" << certPath << "is not valid"; + } else { + d->m_certificate = loadedCerts.at(0); + } + } + return d->m_certificate.isNull(); +} + +void KdeConnectConfig::loadOrGeneratePrivateKeyAndCertificate(const QString &keyPath, const QString &certPath) +{ + bool needsToGenerateKey = loadPrivateKey(keyPath); + bool needsToGenerateCert = needsToGenerateKey || loadCertificate(certPath); if (needsToGenerateKey) { generatePrivateKey(keyPath); } + if (needsToGenerateCert) { + generateCertificate(certPath); + } // Extra security check if (QFile::permissions(keyPath) != strictPermissions) { qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect private key file has too open permissions " << keyPath; } -} - -void KdeConnectConfig::loadCertificate() -{ - QString certPath = certificatePath(); - QFile cert(certPath); - - bool needsToGenerateCert = false; - if (cert.exists() && cert.open(QIODevice::ReadOnly)) { - auto loadedCerts = QSslCertificate::fromPath(certPath); - if (loadedCerts.empty()) { - qCWarning(KDECONNECT_CORE) << "Certificate from" << certPath << "is not valid"; - needsToGenerateCert = true; - } else { - d->m_certificate = loadedCerts.at(0); - } - } else { - needsToGenerateCert = true; - } - - if (needsToGenerateCert) { - generateCertificate(certPath); - } - - // Extra security check if (QFile::permissions(certPath) != strictPermissions) { qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect certificate file has too open permissions " << certPath; } @@ -319,16 +297,16 @@ void KdeConnectConfig::generatePrivateKey(const QString &keyPath) { qCDebug(KDECONNECT_CORE) << "Generating private key"; - bool error = false; - - d->m_privateKey = QCA::KeyGenerator().createRSA(2048); + d->m_privateKey = SslHelper::generateRsaPrivateKey(); + QByteArray keyPem = SslHelper::privateKeyToPEM(d->m_privateKey); QFile privKey(keyPath); + bool error = false; if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate)) { error = true; } else { privKey.setPermissions(strictPermissions); - int written = privKey.write(d->m_privateKey.toPEM().toLatin1()); + int written = privKey.write(keyPem); if (written <= 0) { error = true; } @@ -343,37 +321,22 @@ void KdeConnectConfig::generateCertificate(const QString &certPath) { qCDebug(KDECONNECT_CORE) << "Generating certificate"; - bool error = false; - QString uuid = QUuid::createUuid().toString(); DBusHelper::filterNonExportableCharacters(uuid); qCDebug(KDECONNECT_CORE) << "My id:" << uuid; - // FIXME: We only use QCA here to generate the cert and key, would be nice to get rid of it completely. - // The same thing we are doing with QCA could be done invoking openssl (although it's potentially less portable): - // openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout privateKey.pem -days 3650 -out certificate.pem -subj "/O=KDE/OU=KDE - // Connect/CN=_e6e29ad4_2b31_4b6d_8f7a_9872dbaa9095_" - - QCA::CertificateOptions certificateOptions = QCA::CertificateOptions(); - QDateTime startTime = QDateTime::currentDateTime().addYears(-1); - QDateTime endTime = startTime.addYears(10); - QCA::CertificateInfo certificateInfo; - certificateInfo.insert(QCA::CommonName, uuid); - certificateInfo.insert(QCA::Organization, QStringLiteral("KDE")); - certificateInfo.insert(QCA::OrganizationalUnit, QStringLiteral("Kde connect")); - certificateOptions.setInfo(certificateInfo); - certificateOptions.setFormat(QCA::PKCS10); - certificateOptions.setSerialNumber(QCA::BigInteger(10)); - certificateOptions.setValidityPeriod(startTime, endTime); - - d->m_certificate = QSslCertificate(QCA::Certificate(certificateOptions, d->m_privateKey).toPEM().toLatin1()); + X509 *certificate = SslHelper::generateSelfSignedCertificate(d->m_privateKey, uuid); + QByteArray pemCertificate = SslHelper::certificateToPEM(certificate); + X509_free(certificate); + d->m_certificate = QSslCertificate(pemCertificate); QFile cert(certPath); + bool error = false; if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate)) { error = true; } else { cert.setPermissions(strictPermissions); - int written = cert.write(d->m_certificate.toPem()); + int written = cert.write(pemCertificate); if (written <= 0) { error = true; } diff --git a/core/kdeconnectconfig.h b/core/kdeconnectconfig.h index ba2aa76ae..c04d04a9d 100644 --- a/core/kdeconnectconfig.h +++ b/core/kdeconnectconfig.h @@ -68,9 +68,10 @@ public: private: KdeConnectConfig(); - void loadPrivateKey(); + void loadOrGeneratePrivateKeyAndCertificate(const QString &keyPath, const QString &certPath); + bool loadPrivateKey(const QString &path); + bool loadCertificate(const QString &path); void generatePrivateKey(const QString &path); - void loadCertificate(); void generateCertificate(const QString &path); struct KdeConnectConfigPrivate *d; diff --git a/core/sslhelper.cpp b/core/sslhelper.cpp new file mode 100644 index 000000000..a7d95a932 --- /dev/null +++ b/core/sslhelper.cpp @@ -0,0 +1,100 @@ +/** + * SPDX-FileCopyrightText: 2023 Albert Vaca + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "sslhelper.h" + +#include +#include +#include +#include + +namespace SslHelper +{ + +X509 *generateSelfSignedCertificate(EVP_PKEY *privateKey, const QString &commonName) +{ + X509 *x509 = X509_new(); + X509_set_version(x509, 2); + + // Generate a random serial number for the certificate + BIGNUM *serialNumber = BN_new(); + BN_rand(serialNumber, 160, -1, 0); + ASN1_INTEGER *serial = X509_get_serialNumber(x509); + BN_to_ASN1_INTEGER(serialNumber, serial); + BN_free(serialNumber); + + // Set the certificate subject and issuer (self-signed) + X509_NAME *name = X509_NAME_new(); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast(commonName.toLatin1().data()), -1, -1, 0); // Common Name + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, reinterpret_cast("KDE"), -1, -1, 0); // Organization + X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, reinterpret_cast("KDE Connect"), -1, -1, 0); // Organizational Unit + X509_set_subject_name(x509, name); + X509_set_issuer_name(x509, name); + X509_NAME_free(name); + + // Set the certificate validity period + time_t now = time(nullptr); + int a_year_in_seconds = 356 * 24 * 60 * 60; + ASN1_TIME_set(X509_get_notBefore(x509), now - a_year_in_seconds); + ASN1_TIME_set(X509_get_notAfter(x509), now + 10 * a_year_in_seconds); + + // Set the public key for the certificate + X509_set_pubkey(x509, privateKey); + + // Sign the certificate with the private key + X509_sign(x509, privateKey, EVP_sha256()); + + return x509; +} + +EVP_PKEY *generateRsaPrivateKey() +{ + EVP_PKEY *privateKey = EVP_PKEY_new(); + RSA *rsa = RSA_new(); + + BIGNUM *exponent = BN_new(); + BN_set_word(exponent, RSA_F4); + + RSA_generate_key_ex(rsa, 2048, exponent, nullptr); + EVP_PKEY_assign_RSA(privateKey, rsa); + + BN_free(exponent); + + return privateKey; +} + +QByteArray certificateToPEM(X509 *certificate) +{ + BIO *bio = BIO_new(BIO_s_mem()); + PEM_write_bio_X509(bio, certificate); + BUF_MEM *mem = nullptr; + BIO_get_mem_ptr(bio, &mem); + QByteArray pemData(mem->data, mem->length); + BIO_free_all(bio); + return pemData; +} + +QByteArray privateKeyToPEM(EVP_PKEY *privateKey) +{ + BIO *bio = BIO_new(BIO_s_mem()); + PEM_write_bio_PrivateKey(bio, privateKey, nullptr, nullptr, 0, nullptr, nullptr); + BUF_MEM *mem = nullptr; + BIO_get_mem_ptr(bio, &mem); + QByteArray pemData(mem->data, mem->length); + BIO_free_all(bio); + return pemData; +} + +EVP_PKEY *pemToRsaPrivateKey(const QByteArray &privateKeyPem) +{ + const char *pemData = privateKeyPem.constData(); + BIO *bio = BIO_new_mem_buf(pemData, -1); + EVP_PKEY *privateKey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + BIO_free(bio); + return privateKey; +} + +} // namespace SslHelper diff --git a/core/sslhelper.h b/core/sslhelper.h new file mode 100644 index 000000000..36e855988 --- /dev/null +++ b/core/sslhelper.h @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: 2023 Albert Vaca + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef SSLHELPER_H +#define SSLHELPER_H + +#include +#include + +#include +#include + +namespace SslHelper +{ + +// Delete with X509_free(certificate); +X509 *generateSelfSignedCertificate(EVP_PKEY *privateKey, const QString &commonName); + +// Delete with EVP_PKEY_free(privateKey); +EVP_PKEY *generateRsaPrivateKey(); + +QByteArray certificateToPEM(X509 *certificate); + +QByteArray privateKeyToPEM(EVP_PKEY *privateKey); +EVP_PKEY *pemToRsaPrivateKey(const QByteArray &privateKeyPem); + +} + +#endif \ No newline at end of file