diff --git a/core/daemon.h b/core/daemon.h index 0f4f3b34..56a55fbf 100644 --- a/core/daemon.h +++ b/core/daemon.h @@ -1,101 +1,102 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef KDECONNECT_DAEMON_H #define KDECONNECT_DAEMON_H #include #include #include #include "kdeconnectcore_export.h" #include "device.h" class NetworkPacket; class DeviceLink; class Device; class QNetworkAccessManager; class KDECONNECTCORE_EXPORT Daemon : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.daemon") Q_PROPERTY(bool isDiscoveringDevices READ isDiscoveringDevices) Q_PROPERTY(QStringList pairingRequests READ pairingRequests NOTIFY pairingRequestsChanged) public: explicit Daemon(QObject* parent, bool testMode = false); ~Daemon() override; static Daemon* instance(); QList devicesList() const; virtual void askPairingConfirmation(Device* device) = 0; virtual void reportError(const QString& title, const QString& description) = 0; + virtual void quit() = 0; virtual QNetworkAccessManager* networkAccessManager(); Device* getDevice(const QString& deviceId); QStringList pairingRequests() const; Q_SCRIPTABLE QString selfId() const; public Q_SLOTS: Q_SCRIPTABLE void acquireDiscoveryMode(const QString& id); Q_SCRIPTABLE void releaseDiscoveryMode(const QString& id); Q_SCRIPTABLE void forceOnNetworkChange(); ///don't try to turn into Q_PROPERTY, it doesn't work Q_SCRIPTABLE QString announcedName(); Q_SCRIPTABLE void setAnnouncedName(const QString& name); //Returns a list of ids. The respective devices can be manipulated using the dbus path: "/modules/kdeconnect/Devices/"+id Q_SCRIPTABLE QStringList devices(bool onlyReachable = false, bool onlyPaired = false) const; Q_SCRIPTABLE QMap deviceNames(bool onlyReachable = false, bool onlyPaired = false) const; Q_SCRIPTABLE QString deviceIdByName(const QString& name) const; Q_SCRIPTABLE virtual void sendSimpleNotification(const QString &eventId, const QString &title, const QString &text, const QString &iconName) = 0; Q_SIGNALS: Q_SCRIPTABLE void deviceAdded(const QString& id); Q_SCRIPTABLE void deviceRemoved(const QString& id); //Note that paired devices will never be removed Q_SCRIPTABLE void deviceVisibilityChanged(const QString& id, bool isVisible); Q_SCRIPTABLE void deviceListChanged(); //Emitted when any of deviceAdded, deviceRemoved or deviceVisibilityChanged is emitted Q_SCRIPTABLE void announcedNameChanged(const QString& announcedName); Q_SCRIPTABLE void pairingRequestsChanged(); private Q_SLOTS: void onNewDeviceLink(const NetworkPacket& identityPacket, DeviceLink* dl); void onDeviceStatusChanged(); protected: void addDevice(Device* device); bool isDiscoveringDevices() const; void removeDevice(Device* d); void cleanDevices(); QScopedPointer d; }; #endif diff --git a/core/kdeconnectconfig.cpp b/core/kdeconnectconfig.cpp index 0c385e54..1bb6fe64 100644 --- a/core/kdeconnectconfig.cpp +++ b/core/kdeconnectconfig.cpp @@ -1,273 +1,326 @@ /** * Copyright 2015 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kdeconnectconfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "core_debug.h" #include "dbushelper.h" #include "daemon.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 QSettings* m_config; QSettings* m_trustedDevices; }; KdeConnectConfig* KdeConnectConfig::instance() { static KdeConnectConfig* kcc = new KdeConnectConfig(); return kcc; } 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.")); - return; + Daemon::instance()->quit(); } //Make sure base directory exists QDir().mkpath(baseConfigDir().path()); //.config/kdeconnect/config 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(); } QString KdeConnectConfig::name() { QString username; #ifdef Q_OS_WIN username = qgetenv("USERNAME"); #else username = qgetenv("USER"); #endif QString defaultName = username + '@' + QHostInfo::localHostName(); QString name = d->m_config->value(QStringLiteral("name"), defaultName).toString(); return name; } void KdeConnectConfig::setName(const QString& name) { d->m_config->setValue(QStringLiteral("name"), name); d->m_config->sync(); } QString KdeConnectConfig::deviceType() { return QStringLiteral("desktop"); // TODO } QString KdeConnectConfig::deviceId() { return d->m_certificate.subjectInfo(QSslCertificate::CommonName).constFirst(); } QString KdeConnectConfig::privateKeyPath() { return baseConfigDir().absoluteFilePath(QStringLiteral("privateKey.pem")); } QString KdeConnectConfig::certificatePath() { return baseConfigDir().absoluteFilePath(QStringLiteral("certificate.pem")); } QSslCertificate KdeConnectConfig::certificate() { return d->m_certificate; } QDir KdeConnectConfig::baseConfigDir() { QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); QString kdeconnectConfigPath = QDir(configPath).absoluteFilePath(QStringLiteral("kdeconnect")); return QDir(kdeconnectConfigPath); } QStringList KdeConnectConfig::trustedDevices() { const QStringList& list = d->m_trustedDevices->childGroups(); return list; } void KdeConnectConfig::addTrustedDevice(const QString& id, const QString& name, const QString& type) { d->m_trustedDevices->beginGroup(id); d->m_trustedDevices->setValue(QStringLiteral("name"), name); d->m_trustedDevices->setValue(QStringLiteral("type"), type); d->m_trustedDevices->endGroup(); d->m_trustedDevices->sync(); QDir().mkpath(deviceConfigDir(id).path()); } KdeConnectConfig::DeviceInfo KdeConnectConfig::getTrustedDevice(const QString& id) { d->m_trustedDevices->beginGroup(id); KdeConnectConfig::DeviceInfo info; info.deviceName = d->m_trustedDevices->value(QStringLiteral("name"), QLatin1String("unnamed")).toString(); info.deviceType = d->m_trustedDevices->value(QStringLiteral("type"), QLatin1String("unknown")).toString(); d->m_trustedDevices->endGroup(); return info; } void KdeConnectConfig::removeTrustedDevice(const QString& deviceId) { d->m_trustedDevices->remove(deviceId); d->m_trustedDevices->sync(); //We do not remove the config files. } // Utility functions to set and get a value void KdeConnectConfig::setDeviceProperty(const QString& deviceId, const QString& key, const QString& value) { d->m_trustedDevices->beginGroup(deviceId); d->m_trustedDevices->setValue(key, value); d->m_trustedDevices->endGroup(); d->m_trustedDevices->sync(); } QString KdeConnectConfig::getDeviceProperty(const QString& deviceId, const QString& key, const QString& defaultValue) { QString value; d->m_trustedDevices->beginGroup(deviceId); value = d->m_trustedDevices->value(key, defaultValue).toString(); d->m_trustedDevices->endGroup(); return value; } QDir KdeConnectConfig::deviceConfigDir(const QString& deviceId) { QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId); return QDir(deviceConfigPath); } QDir KdeConnectConfig::pluginConfigDir(const QString& deviceId, const QString& pluginName) { QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId); QString pluginConfigDir = QDir(deviceConfigPath).absoluteFilePath(pluginName); return QDir(pluginConfigDir); } void KdeConnectConfig::loadPrivateKey() { QString keyPath = privateKeyPath(); QFile privKey(keyPath); - if (privKey.exists() && privKey.open(QIODevice::ReadOnly)) { - - d->m_privateKey = QCA::PrivateKey::fromPEM(privKey.readAll()); + bool needsToGenerateKey = false; + if (privKey.exists() && privKey.open(QIODevice::ReadOnly) && privKey.size() > 0) { + QCA::ConvertResult result; + d->m_privateKey = QCA::PrivateKey::fromPEM(privKey.readAll(), QCA::SecureArray(), &result); + if (result != QCA::ConvertResult::ConvertGood) { + needsToGenerateKey = true; + } } else { + needsToGenerateKey = true; + } - d->m_privateKey = QCA::KeyGenerator().createRSA(2048); - - if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store private key file: %1", keyPath)); - } else { - privKey.setPermissions(strict); - privKey.write(d->m_privateKey.toPEM().toLatin1()); - } + if (needsToGenerateKey) { + generatePrivateKey(keyPath); } //Extra security check - if (QFile::permissions(keyPath) != strict) { + if (QFile::permissions(keyPath) != strictPermissions) { qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect private key file has too open permissions " << keyPath; } } -void KdeConnectConfig::loadCertificate() +void KdeConnectConfig::generatePrivateKey(const QString& keyPath) { - QString certPath = certificatePath(); - QFile cert(certPath); - if (cert.exists() && cert.open(QIODevice::ReadOnly)) { + bool error = false; - d->m_certificate = QSslCertificate::fromPath(certPath).at(0); + d->m_privateKey = QCA::KeyGenerator().createRSA(2048); + QFile privKey(keyPath); + if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + error = true; } else { + privKey.setPermissions(strictPermissions); + int written = privKey.write(d->m_privateKey.toPEM().toLatin1()); + if (written <= 0) { + error = true; + } + } - // No certificate yet. Probably first run. Let's generate one! + if (error) { + Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store private key file: %1", keyPath)); + Daemon::instance()->quit(); + } - 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_" +void KdeConnectConfig::loadCertificate() +{ + QString certPath = certificatePath(); + QFile cert(certPath); - 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); + bool needsToGenerateCert = false; + if (cert.exists() && cert.open(QIODevice::ReadOnly) && cert.size() > 0) { + auto loadedCerts = QSslCertificate::fromPath(certPath); + if (loadedCerts.empty()) { + needsToGenerateCert = true; + } else { + d->m_certificate = loadedCerts.at(0); + } + } else { + needsToGenerateCert = true; + } - d->m_certificate = QSslCertificate(QCA::Certificate(certificateOptions, d->m_privateKey).toPEM().toLatin1()); + if (needsToGenerateCert) { + generateCertificate(certPath); + } - if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store certificate file: %1", certPath)); - } else { - cert.setPermissions(strict); - cert.write(d->m_certificate.toPem()); + //Extra security check + if (QFile::permissions(certPath) != strictPermissions) { + qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect certificate file has too open permissions " << certPath; + } +} + +void KdeConnectConfig::generateCertificate(const QString& certPath) +{ + 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()); + + QFile cert(certPath); + if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate)) { + error = true; + } else { + cert.setPermissions(strictPermissions); + int written = cert.write(d->m_certificate.toPem()); + if (written <= 0) { + error = true; } } + + if (error) { + Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store certificate file: %1", certPath)); + Daemon::instance()->quit(); + } } diff --git a/core/kdeconnectconfig.h b/core/kdeconnectconfig.h index 9e758f9a..003215d4 100644 --- a/core/kdeconnectconfig.h +++ b/core/kdeconnectconfig.h @@ -1,85 +1,84 @@ /** * Copyright 2015 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef KDECONNECTCONFIG_H #define KDECONNECTCONFIG_H #include #include "kdeconnectcore_export.h" class QSslCertificate; class KDECONNECTCORE_EXPORT KdeConnectConfig { public: struct DeviceInfo { QString deviceName; QString deviceType; }; static KdeConnectConfig* instance(); /* * Our own info */ QString deviceId(); QString name(); QString deviceType(); QString privateKeyPath(); QString certificatePath(); QSslCertificate certificate(); void setName(const QString& name); /* * Trusted devices */ QStringList trustedDevices(); //list of ids void removeTrustedDevice(const QString& id); void addTrustedDevice(const QString& id, const QString& name, const QString& type); KdeConnectConfig::DeviceInfo getTrustedDevice(const QString& id); void setDeviceProperty(const QString& deviceId, const QString& name, const QString& value); QString getDeviceProperty(const QString& deviceId, const QString& name, const QString& defaultValue = QString()); /* * Paths for config files, there is no guarantee the directories already exist */ QDir baseConfigDir(); QDir deviceConfigDir(const QString& deviceId); QDir pluginConfigDir(const QString& deviceId, const QString& pluginName); //Used by KdeConnectPluginConfig private: KdeConnectConfig(); void loadPrivateKey(); + void generatePrivateKey(const QString& path); void loadCertificate(); + void generateCertificate(const QString& path); struct KdeConnectConfigPrivate* d; - - const QFile::Permissions strict = QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser; - }; #endif diff --git a/daemon/kdeconnectd.cpp b/daemon/kdeconnectd.cpp index c451fd6f..fa120a20 100644 --- a/daemon/kdeconnectd.cpp +++ b/daemon/kdeconnectd.cpp @@ -1,107 +1,115 @@ /** * Copyright 2014 Yuri Samoilenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include +#include #include #include #include #include #include #include "core/daemon.h" #include "core/device.h" #include "core/backends/pairinghandler.h" #include "kdeconnect-version.h" +Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_DAEMON) +Q_LOGGING_CATEGORY(KDECONNECT_DAEMON, "kdeconnect.daemon") + class DesktopDaemon : public Daemon { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.daemon") public: DesktopDaemon(QObject* parent = nullptr) : Daemon(parent) , m_nam(nullptr) {} void askPairingConfirmation(Device* device) override { KNotification* notification = new KNotification(QStringLiteral("pairingRequest")); notification->setIconName(QStringLiteral("dialog-information")); notification->setComponentName(QStringLiteral("kdeconnect")); notification->setText(i18n("Pairing request from %1", device->name().toHtmlEscaped())); notification->setActions(QStringList() << i18n("Accept") << i18n("Reject")); // notification->setTimeout(PairingHandler::pairingTimeoutMsec()); connect(notification, &KNotification::action1Activated, device, &Device::acceptPairing); connect(notification, &KNotification::action2Activated, device, &Device::rejectPairing); notification->sendEvent(); } void reportError(const QString & title, const QString & description) override { + QCWarning(KDECONNECT_DAEMON) << title << ":" << description; KNotification::event(KNotification::Error, title, description); } QNetworkAccessManager* networkAccessManager() override { if (!m_nam) { m_nam = new KIO::AccessManager(this); } return m_nam; } Q_SCRIPTABLE void sendSimpleNotification(const QString &eventId, const QString &title, const QString &text, const QString &iconName) override { KNotification* notification = new KNotification(eventId); //KNotification::Persistent notification->setIconName(iconName); notification->setComponentName(QStringLiteral("kdeconnect")); notification->setTitle(title); notification->setText(text); notification->sendEvent(); } + void quit() override { + QApplication::quit(); + } private: QNetworkAccessManager* m_nam; }; int main(int argc, char* argv[]) { QApplication app(argc, argv); KAboutData aboutData( QStringLiteral("kdeconnectd"), i18n("KDE Connect Daemon"), QStringLiteral(KDECONNECT_VERSION_STRING), i18n("KDE Connect Daemon"), KAboutLicense::GPL ); KAboutData::setApplicationData(aboutData); app.setQuitOnLastWindowClosed(false); KDBusService dbusService(KDBusService::Unique); DesktopDaemon daemon; return app.exec(); } #include "kdeconnectd.moc" diff --git a/tests/testdaemon.h b/tests/testdaemon.h index 15e89062..3144c601 100644 --- a/tests/testdaemon.h +++ b/tests/testdaemon.h @@ -1,66 +1,70 @@ /** * Copyright 2015 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef TESTDAEMON_H #define TESTDAEMON_H #include #include class TestDaemon : public Daemon { public: TestDaemon(QObject* parent = nullptr) : Daemon(parent, true) , m_nam(nullptr) { } void addDevice(Device* device) { Daemon::addDevice(device); } void reportError(const QString & title, const QString & description) override { qWarning() << "error:" << title << description; } void askPairingConfirmation(Device * d) override { d->acceptPairing(); } QNetworkAccessManager* networkAccessManager() override { if (!m_nam) { m_nam = new KIO::AccessManager(this); } return m_nam; } Q_SCRIPTABLE virtual void sendSimpleNotification(const QString &eventId, const QString &title, const QString &text, const QString &iconName) override { qDebug() << eventId << title << text << iconName; } + void quit() override { + qDebug() << "quit was called"; + } + private: QNetworkAccessManager* m_nam; }; #endif