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