1 /**
2  * SPDX-FileCopyrightText: 2015 Albert Vaca <albertvaka@gmail.com>
3  *
4  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5  */
6 
7 #include "kdeconnectconfig.h"
8 
9 #include <KLocalizedString>
10 
11 #include <QFile>
12 #include <QDebug>
13 #include <QFileInfo>
14 #include <QUuid>
15 #include <QDir>
16 #include <QStandardPaths>
17 #include <QCoreApplication>
18 #include <QHostInfo>
19 #include <QSettings>
20 #include <QSslCertificate>
21 #include <QtCrypto>
22 #include <QThread>
23 
24 #include "core_debug.h"
25 #include "dbushelper.h"
26 #include "daemon.h"
27 
28 const QFile::Permissions strictPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser;
29 
30 struct KdeConnectConfigPrivate {
31 
32     // The Initializer object sets things up, and also does cleanup when it goes out of scope
33     // Note it's not being used anywhere. That's intended
34     QCA::Initializer m_qcaInitializer;
35 
36     QCA::PrivateKey m_privateKey;
37     QSslCertificate m_certificate; // Use QSslCertificate instead of QCA::Certificate due to compatibility with QSslSocket
38 
39     QSettings* m_config;
40     QSettings* m_trustedDevices;
41 
42 #ifdef USE_PRIVATE_DBUS
43     QString m_privateDBusAddress;  // Private DBus Address cache
44 #endif
45 };
46 
getDefaultDeviceName()47 static QString getDefaultDeviceName() {
48 #ifdef SAILFISHOS
49     const QString hwReleaseFile = QStringLiteral("/etc/hw-release");
50     // QSettings will crash if the file does not exist or can be created, like in this case by us in /etc.
51     // E.g. in the SFOS SDK Emulator there is no such file, so check before to protect against the crash.
52     if (QFile::exists(hwReleaseFile)) {
53         QSettings hwRelease(hwReleaseFile, QSettings::IniFormat);
54         auto hwName = hwRelease.value(QStringLiteral("NAME")).toString();
55         if (!hwName.isEmpty()) {
56             return hwName;
57         }
58     }
59 #endif
60 
61     return QHostInfo::localHostName();
62 }
63 
64 
instance()65 KdeConnectConfig& KdeConnectConfig::instance()
66 {
67     static KdeConnectConfig kcc;
68     return kcc;
69 }
70 
KdeConnectConfig()71 KdeConnectConfig::KdeConnectConfig()
72     : d(new KdeConnectConfigPrivate)
73 {
74     //qCDebug(KDECONNECT_CORE) << "QCA supported capabilities:" << QCA::supportedFeatures().join(",");
75     if(!QCA::isSupported("rsa")) {
76         qCritical() << "Could not find support for RSA in your QCA installation";
77         Daemon::instance()->reportError(
78                              i18n("KDE Connect failed to start"),
79                              i18n("Could not find support for RSA in your QCA installation. If your "
80                                   "distribution provides separate packets for QCA-ossl and QCA-gnupg, "
81                                   "make sure you have them installed and try again."));
82     }
83 
84     //Make sure base directory exists
85     QDir().mkpath(baseConfigDir().path());
86 
87     //.config/kdeconnect/config
88     d->m_config = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("config")), QSettings::IniFormat);
89     d->m_trustedDevices = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("trusted_devices")), QSettings::IniFormat);
90 
91     loadPrivateKey();
92     loadCertificate();
93 
94     if (name().isEmpty()) {
95         setName(getDefaultDeviceName());
96     }
97 }
98 
name()99 QString KdeConnectConfig::name()
100 {
101     return d->m_config->value(QStringLiteral("name")).toString();
102 }
103 
setName(const QString & name)104 void KdeConnectConfig::setName(const QString& name)
105 {
106     d->m_config->setValue(QStringLiteral("name"), name);
107     d->m_config->sync();
108 }
109 
deviceType()110 QString KdeConnectConfig::deviceType()
111 {
112 #ifdef SAILFISHOS
113     return QStringLiteral("phone");
114 #else
115     const QByteArrayList platforms = qgetenv("PLASMA_PLATFORM").split(':');
116 
117     if (platforms.contains("phone")) {
118         return QStringLiteral("phone");
119     } else if (platforms.contains("tablet")) {
120         return QStringLiteral("tablet");
121     } else if(platforms.contains("mediacenter")) {
122         return QStringLiteral("tv");
123     }
124 
125     // TODO non-Plasma mobile platforms
126 
127     return QStringLiteral("desktop");
128 #endif
129 }
130 
deviceId()131 QString KdeConnectConfig::deviceId()
132 {
133     return d->m_certificate.subjectInfo(QSslCertificate::CommonName).constFirst();
134 }
135 
privateKeyPath()136 QString KdeConnectConfig::privateKeyPath()
137 {
138     return baseConfigDir().absoluteFilePath(QStringLiteral("privateKey.pem"));
139 }
140 
certificatePath()141 QString KdeConnectConfig::certificatePath()
142 {
143     return baseConfigDir().absoluteFilePath(QStringLiteral("certificate.pem"));
144 }
145 
certificate()146 QSslCertificate KdeConnectConfig::certificate()
147 {
148     return d->m_certificate;
149 }
150 
baseConfigDir()151 QDir KdeConnectConfig::baseConfigDir()
152 {
153     QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
154     QString kdeconnectConfigPath = QDir(configPath).absoluteFilePath(QStringLiteral("kdeconnect"));
155     return QDir(kdeconnectConfigPath);
156 }
157 
trustedDevices()158 QStringList KdeConnectConfig::trustedDevices()
159 {
160     const QStringList& list = d->m_trustedDevices->childGroups();
161     return list;
162 }
163 
164 
addTrustedDevice(const QString & id,const QString & name,const QString & type)165 void KdeConnectConfig::addTrustedDevice(const QString& id, const QString& name, const QString& type)
166 {
167     d->m_trustedDevices->beginGroup(id);
168     d->m_trustedDevices->setValue(QStringLiteral("name"), name);
169     d->m_trustedDevices->setValue(QStringLiteral("type"), type);
170     d->m_trustedDevices->endGroup();
171     d->m_trustedDevices->sync();
172 
173     QDir().mkpath(deviceConfigDir(id).path());
174 }
175 
getTrustedDevice(const QString & id)176 KdeConnectConfig::DeviceInfo KdeConnectConfig::getTrustedDevice(const QString& id)
177 {
178     d->m_trustedDevices->beginGroup(id);
179 
180     KdeConnectConfig::DeviceInfo info;
181     info.deviceName = d->m_trustedDevices->value(QStringLiteral("name"), QLatin1String("unnamed")).toString();
182     info.deviceType = d->m_trustedDevices->value(QStringLiteral("type"), QLatin1String("unknown")).toString();
183 
184     d->m_trustedDevices->endGroup();
185     return info;
186 }
187 
removeTrustedDevice(const QString & deviceId)188 void KdeConnectConfig::removeTrustedDevice(const QString& deviceId)
189 {
190     d->m_trustedDevices->remove(deviceId);
191     d->m_trustedDevices->sync();
192     //We do not remove the config files.
193 }
194 
195 // Utility functions to set and get a value
setDeviceProperty(const QString & deviceId,const QString & key,const QString & value)196 void KdeConnectConfig::setDeviceProperty(const QString& deviceId, const QString& key, const QString& value)
197 {
198     // do not store values for untrusted devices (it would make them trusted)
199     if (!trustedDevices().contains(deviceId))
200         return;
201 
202     d->m_trustedDevices->beginGroup(deviceId);
203     d->m_trustedDevices->setValue(key, value);
204     d->m_trustedDevices->endGroup();
205     d->m_trustedDevices->sync();
206 }
207 
getDeviceProperty(const QString & deviceId,const QString & key,const QString & defaultValue)208 QString KdeConnectConfig::getDeviceProperty(const QString& deviceId, const QString& key, const QString& defaultValue)
209 {
210     QString value;
211     d->m_trustedDevices->beginGroup(deviceId);
212     value = d->m_trustedDevices->value(key, defaultValue).toString();
213     d->m_trustedDevices->endGroup();
214     return value;
215 }
216 
setCustomDevices(const QStringList & addresses)217 void KdeConnectConfig::setCustomDevices(const QStringList& addresses)
218 {
219     d->m_config->setValue(QStringLiteral("customDevices"), addresses);
220     d->m_config->sync();
221 }
222 
customDevices() const223 QStringList KdeConnectConfig::customDevices() const
224 {
225     return d->m_config->value(QStringLiteral("customDevices")).toStringList();
226 }
227 
deviceConfigDir(const QString & deviceId)228 QDir KdeConnectConfig::deviceConfigDir(const QString& deviceId)
229 {
230     QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId);
231     return QDir(deviceConfigPath);
232 }
233 
pluginConfigDir(const QString & deviceId,const QString & pluginName)234 QDir KdeConnectConfig::pluginConfigDir(const QString& deviceId, const QString& pluginName)
235 {
236     QString deviceConfigPath = baseConfigDir().absoluteFilePath(deviceId);
237     QString pluginConfigDir = QDir(deviceConfigPath).absoluteFilePath(pluginName);
238     return QDir(pluginConfigDir);
239 }
240 
loadPrivateKey()241 void KdeConnectConfig::loadPrivateKey()
242 {
243     QString keyPath = privateKeyPath();
244     QFile privKey(keyPath);
245 
246     bool needsToGenerateKey = false;
247     if (privKey.exists() && privKey.open(QIODevice::ReadOnly)) {
248         QCA::ConvertResult result;
249         d->m_privateKey = QCA::PrivateKey::fromPEM(QString::fromLatin1(privKey.readAll()), QCA::SecureArray(), &result);
250         if (result != QCA::ConvertResult::ConvertGood) {
251             qCWarning(KDECONNECT_CORE) << "Private key from" << keyPath << "is not valid";
252             needsToGenerateKey = true;
253         }
254     } else {
255         needsToGenerateKey = true;
256     }
257 
258     if (needsToGenerateKey) {
259         generatePrivateKey(keyPath);
260     }
261 
262     //Extra security check
263     if (QFile::permissions(keyPath) != strictPermissions) {
264         qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect private key file has too open permissions " << keyPath;
265     }
266 }
267 
loadCertificate()268 void KdeConnectConfig::loadCertificate()
269 {
270     QString certPath = certificatePath();
271     QFile cert(certPath);
272 
273     bool needsToGenerateCert = false;
274     if (cert.exists() && cert.open(QIODevice::ReadOnly)) {
275         auto loadedCerts = QSslCertificate::fromPath(certPath);
276         if (loadedCerts.empty()) {
277             qCWarning(KDECONNECT_CORE) << "Certificate from" << certPath << "is not valid";
278             needsToGenerateCert = true;
279         } else {
280             d->m_certificate = loadedCerts.at(0);
281         }
282     } else {
283         needsToGenerateCert = true;
284     }
285 
286     if (needsToGenerateCert) {
287         generateCertificate(certPath);
288     }
289 
290     //Extra security check
291     if (QFile::permissions(certPath) != strictPermissions) {
292         qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect certificate file has too open permissions " << certPath;
293     }
294 }
295 
generatePrivateKey(const QString & keyPath)296 void KdeConnectConfig::generatePrivateKey(const QString& keyPath)
297 {
298     qCDebug(KDECONNECT_CORE) << "Generating private key";
299 
300     bool error = false;
301 
302     d->m_privateKey = QCA::KeyGenerator().createRSA(2048);
303 
304     QFile privKey(keyPath);
305     if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate))  {
306         error = true;
307     } else {
308         privKey.setPermissions(strictPermissions);
309         int written = privKey.write(d->m_privateKey.toPEM().toLatin1());
310         if (written <= 0) {
311             error = true;
312         }
313     }
314 
315     if (error) {
316         Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store private key file: %1", keyPath));
317     }
318 
319 }
320 
generateCertificate(const QString & certPath)321 void KdeConnectConfig::generateCertificate(const QString& certPath)
322 {
323     qCDebug(KDECONNECT_CORE) << "Generating certificate";
324 
325     bool error = false;
326 
327     QString uuid = QUuid::createUuid().toString();
328     DBusHelper::filterNonExportableCharacters(uuid);
329     qCDebug(KDECONNECT_CORE) << "My id:" << uuid;
330 
331     // FIXME: We only use QCA here to generate the cert and key, would be nice to get rid of it completely.
332     // The same thing we are doing with QCA could be done invoking openssl (although it's potentially less portable):
333     // 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_"
334 
335     QCA::CertificateOptions certificateOptions = QCA::CertificateOptions();
336     QDateTime startTime = QDateTime::currentDateTime().addYears(-1);
337     QDateTime endTime = startTime.addYears(10);
338     QCA::CertificateInfo certificateInfo;
339     certificateInfo.insert(QCA::CommonName, uuid);
340     certificateInfo.insert(QCA::Organization,QStringLiteral("KDE"));
341     certificateInfo.insert(QCA::OrganizationalUnit,QStringLiteral("Kde connect"));
342     certificateOptions.setInfo(certificateInfo);
343     certificateOptions.setFormat(QCA::PKCS10);
344     certificateOptions.setSerialNumber(QCA::BigInteger(10));
345     certificateOptions.setValidityPeriod(startTime, endTime);
346 
347     d->m_certificate = QSslCertificate(QCA::Certificate(certificateOptions, d->m_privateKey).toPEM().toLatin1());
348 
349     QFile cert(certPath);
350     if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate))  {
351         error = true;
352     } else {
353         cert.setPermissions(strictPermissions);
354         int written = cert.write(d->m_certificate.toPem());
355         if (written <= 0) {
356             error = true;
357         }
358     }
359 
360     if (error) {
361         Daemon::instance()->reportError(QStringLiteral("KDE Connect"), i18n("Could not store certificate file: %1", certPath));
362     }
363 }
364 
365 #ifdef USE_PRIVATE_DBUS
privateDBusAddressPath()366 QString KdeConnectConfig::privateDBusAddressPath()
367 {
368     return baseConfigDir().absoluteFilePath(QStringLiteral("private_dbus_address"));
369 }
370 
privateDBusAddress()371 QString KdeConnectConfig::privateDBusAddress()
372 {
373     if (d->m_privateDBusAddress.length() != 0) return d->m_privateDBusAddress;
374 
375     QString dbusAddressPath = privateDBusAddressPath();
376     QFile dbusAddressFile(dbusAddressPath);
377 
378     if (!dbusAddressFile.open(QFile::ReadOnly | QFile::Text)) {
379         qCCritical(KDECONNECT_CORE) << "Private DBus enabled but error read private dbus address conf";
380         exit(1);
381     }
382 
383     QTextStream in(&dbusAddressFile);
384 
385     qCDebug(KDECONNECT_CORE) << "Waiting for private dbus";
386 
387     int retry = 0;
388     QString addr = in.readLine();
389     while(addr.length() == 0 && retry < 5) {
390         qCDebug(KDECONNECT_CORE) << "Retry reading private DBus address after 3s";
391         QThread::sleep(3);
392         retry ++;
393         addr = in.readLine();   // Read until first not empty line
394     }
395 
396     if (addr.length() == 0) {
397         qCCritical(KDECONNECT_CORE) << "Private DBus enabled but read private dbus address failed";
398         exit(1);
399     }
400 
401     qCDebug(KDECONNECT_CORE) << "Private dbus address: " << addr;
402 
403     d->m_privateDBusAddress = addr;
404 
405     return addr;
406 }
407 #endif
408