File indexing completed on 2024-04-28 05:34:17

0001 // SPDX-FileCopyrightText: 2021 Daniel Vrátil <dvratil@kde.org>
0002 //
0003 // SPDX-License-Identifier: LGPL-2.1-or-later
0004 
0005 #include "providerbase.h"
0006 #include "klipperinterface.h"
0007 #include "plasmapass_debug.h"
0008 
0009 #include <QClipboard>
0010 #include <QCryptographicHash>
0011 #include <QGuiApplication>
0012 #include <QMimeData>
0013 #include <QProcess>
0014 #include <QStandardPaths>
0015 
0016 #include <QDBusConnection>
0017 
0018 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0019 #include <Plasma/DataEngine>
0020 #include <Plasma/DataEngineConsumer>
0021 #include <Plasma/PluginLoader>
0022 #include <Plasma/Service>
0023 #include <Plasma/ServiceJob>
0024 #else
0025 #include <Plasma5Support/DataEngine>
0026 #include <Plasma5Support/DataEngineConsumer>
0027 #include <Plasma5Support/PluginLoader>
0028 #include <Plasma5Support/Service>
0029 #include <Plasma5Support/ServiceJob>
0030 #endif
0031 
0032 #include <KLocalizedString>
0033 
0034 #include <chrono>
0035 #include <utility>
0036 
0037 #include <QGpgME/DecryptJob>
0038 #include <QGpgME/Protocol>
0039 #include <gpgme++/decryptionresult.h>
0040 
0041 using namespace std::chrono;
0042 using namespace std::chrono_literals;
0043 using namespace PlasmaPass;
0044 
0045 namespace
0046 {
0047 constexpr const auto DefaultSecretTimeout = 45s;
0048 constexpr const auto SecretTimeoutUpdateInterval = 100ms;
0049 
0050 const QString klipperDBusService = QStringLiteral("org.kde.klipper");
0051 const QString klipperDBusPath = QStringLiteral("/klipper");
0052 const QString klipperDataEngine = QStringLiteral("org.kde.plasma.clipboard");
0053 
0054 }
0055 
0056 KlipperUtils::State ProviderBase::sKlipperState = KlipperUtils::State::Unknown;
0057 
0058 ProviderBase::ProviderBase(const QString &path, QObject *parent)
0059     : QObject(parent)
0060     , mPath(path)
0061     , mSecretTimeout(DefaultSecretTimeout)
0062 {
0063     mTimer.setInterval(SecretTimeoutUpdateInterval);
0064     connect(&mTimer, &QTimer::timeout, this, [this]() {
0065         mTimeout -= mTimer.interval();
0066         Q_EMIT timeoutChanged();
0067         if (mTimeout == 0) {
0068             expireSecret();
0069         }
0070     });
0071 
0072     QTimer::singleShot(0, this, &ProviderBase::start);
0073 }
0074 
0075 ProviderBase::~ProviderBase() = default;
0076 
0077 void ProviderBase::start()
0078 {
0079     QFile file(mPath);
0080     if (!file.open(QIODevice::ReadOnly)) {
0081         qCWarning(PLASMAPASS_LOG, "Failed to open password file: %s", qUtf8Printable(file.errorString()));
0082         setError(i18n("Failed to open password file: %1", file.errorString()));
0083         return;
0084     }
0085 
0086     auto decryptJob = QGpgME::openpgp()->decryptJob();
0087     connect(decryptJob, &QGpgME::DecryptJob::result, this, [this](const GpgME::DecryptionResult &result, const QByteArray &plainText) {
0088         if (result.error()) {
0089             qCWarning(PLASMAPASS_LOG, "Failed to decrypt password: %s", result.error().asString());
0090             setError(i18n("Failed to decrypt password: %1", QString::fromUtf8(result.error().asString())));
0091             return;
0092         }
0093 
0094         const auto data = QString::fromUtf8(plainText);
0095         if (data.isEmpty()) {
0096             qCWarning(PLASMAPASS_LOG, "Password file is empty!");
0097             setError(i18n("No password found"));
0098             return;
0099         }
0100 
0101 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0102         const auto lines = data.splitRef(QLatin1Char('\n'));
0103 #else
0104         const auto lines = QStringView(data).split(QLatin1Char('\n'));
0105 #endif
0106         for (const auto &line : lines) {
0107             if (handleSecret(line) == HandlingResult::Stop) {
0108                 break;
0109             }
0110         }
0111     });
0112 
0113     const auto error = decryptJob->start(file.readAll());
0114     if (error) {
0115         qCWarning(PLASMAPASS_LOG, "Failed to decrypt password: %s", error.asString());
0116         setError(i18n("Failed to decrypt password: %1", QString::fromUtf8(error.asString())));
0117         return;
0118     }
0119 }
0120 
0121 bool ProviderBase::isValid() const
0122 {
0123     return !mSecret.isNull();
0124 }
0125 
0126 QString ProviderBase::secret() const
0127 {
0128     return mSecret;
0129 }
0130 
0131 namespace {
0132 
0133 QMimeData *mimeDataForPassword(const QString &password)
0134 {
0135     auto mimeData = new QMimeData;
0136     mimeData->setText(password);
0137     // https://phabricator.kde.org/D12539
0138     mimeData->setData(QStringLiteral("x-kde-passwordManagerHint"), "secret");
0139     return mimeData;
0140 }
0141 
0142 } // namespace
0143 
0144 void ProviderBase::setSecret(const QString &secret)
0145 {
0146     auto clipboard = qGuiApp->clipboard(); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
0147     clipboard->setMimeData(mimeDataForPassword(secret), QClipboard::Clipboard);
0148 
0149     if (clipboard->supportsSelection()) {
0150         clipboard->setMimeData(mimeDataForPassword(secret), QClipboard::Selection);
0151     }
0152 
0153     mSecret = secret;
0154     Q_EMIT validChanged();
0155     Q_EMIT secretChanged();
0156 
0157     mTimeout = defaultTimeout();
0158     Q_EMIT timeoutChanged();
0159     mTimer.start();
0160 }
0161 
0162 void ProviderBase::setSecretTimeout(std::chrono::seconds timeout)
0163 {
0164     mSecretTimeout = timeout;
0165 }
0166 
0167 void ProviderBase::expireSecret()
0168 {
0169     removePasswordFromClipboard(mSecret);
0170 
0171     mSecret.clear();
0172     mTimer.stop();
0173     Q_EMIT validChanged();
0174     Q_EMIT secretChanged();
0175 
0176     // Delete the provider, it's no longer needed
0177     deleteLater();
0178 }
0179 
0180 int ProviderBase::timeout() const
0181 {
0182     return mTimeout;
0183 }
0184 
0185 int ProviderBase::defaultTimeout() const
0186 {
0187     return duration_cast<milliseconds>(mSecretTimeout).count();
0188 }
0189 
0190 QString ProviderBase::error() const
0191 {
0192     return mError;
0193 }
0194 
0195 bool ProviderBase::hasError() const
0196 {
0197     return !mError.isNull();
0198 }
0199 
0200 void ProviderBase::setError(const QString &error)
0201 {
0202     mError = error;
0203     Q_EMIT errorChanged();
0204 }
0205 
0206 void ProviderBase::reset()
0207 {
0208     mError.clear();
0209     mSecret.clear();
0210     mTimer.stop();
0211     Q_EMIT errorChanged();
0212     Q_EMIT validChanged();
0213     Q_EMIT secretChanged();
0214 
0215     QTimer::singleShot(0, this, &ProviderBase::start);
0216 }
0217 
0218 void ProviderBase::removePasswordFromClipboard(const QString &password)
0219 {
0220     // Clear the WS clipboard itself
0221     const auto clipboard = qGuiApp->clipboard(); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
0222     if (clipboard->text() == password) {
0223         clipboard->clear();
0224     }
0225 
0226     if (sKlipperState == KlipperUtils::State::Unknown) {
0227         sKlipperState = KlipperUtils::getState();
0228     }
0229 
0230     switch (sKlipperState) {
0231     case KlipperUtils::State::Unknown:
0232     case KlipperUtils::State::Missing:
0233         qCDebug(PLASMAPASS_LOG, "Klipper not detected in the system, will not attempt to clear the clipboard history");
0234         return;
0235     case KlipperUtils::State::SupportsPasswordManagerHint:
0236         // Klipper is not present in the system or is recent enough that it
0237         // supports the x-kde-passwordManagerHint in which case we don't need to
0238         // ask it to remove the password from its history - it would fail since the
0239         // password is not there and we would end up clearing user's entire clipboard
0240         // history.
0241         qCDebug(PLASMAPASS_LOG, "Klipper with support for x-kde-passwordManagerHint detected, will not attempt to clear the clipboard history");
0242         return;
0243     case KlipperUtils::State::Available:
0244         // Klipper is available but is too old to support x-kde-passwordManagerHint so
0245         // we have to attempt to clear the password manually.
0246         qCDebug(PLASMAPASS_LOG, "Old Klipper without x-kde-passwordManagerHint support detected, will attempt to remove the password from clipboard history");
0247         break;
0248     }
0249 
0250     if (!mEngineConsumer) {
0251 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0252         mEngineConsumer = std::make_unique<Plasma::DataEngineConsumer>();
0253 #else
0254         mEngineConsumer = std::make_unique<Plasma5Support::DataEngineConsumer>();
0255 #endif
0256     }
0257     auto engine = mEngineConsumer->dataEngine(klipperDataEngine);
0258 
0259     // Klipper internally identifies each history entry by its SHA1 hash
0260     // (see klipper/historystringitem.cpp) so we try here to obtain a service directly
0261     // for the history item with our password so that we can only remove the
0262     // password from the history without having to clear the entire history.
0263     const auto service = engine->serviceForSource(QString::fromLatin1(QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha1).toBase64()));
0264     if (service == nullptr) {
0265         qCWarning(PLASMAPASS_LOG, "Failed to obtain PlasmaService for the password, falling back to clearClipboard()");
0266         mEngineConsumer.reset();
0267         clearClipboard();
0268         return;
0269     }
0270 
0271     auto job = service->startOperationCall(service->operationDescription(QStringLiteral("remove")));
0272 
0273     connect(job, &KJob::result, this, &ProviderBase::onPlasmaServiceRemovePasswordResult);
0274 }
0275 
0276 void ProviderBase::onPlasmaServiceRemovePasswordResult(KJob *job)
0277 {
0278     // Disconnect from the job: Klipper's ClipboardJob is buggy and emits result() twice
0279     disconnect(job, &KJob::result, this, &ProviderBase::onPlasmaServiceRemovePasswordResult);
0280     QTimer::singleShot(0, this, [this]() {
0281         mEngineConsumer.reset();
0282     });
0283 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0284     auto serviceJob = qobject_cast<Plasma::ServiceJob *>(job);
0285 #else
0286     auto serviceJob = qobject_cast<Plasma5Support::ServiceJob *>(job);
0287 #endif
0288     if (serviceJob->error() != 0) {
0289         qCWarning(PLASMAPASS_LOG, "ServiceJob for clipboard failed: %s", qUtf8Printable(serviceJob->errorString()));
0290         clearClipboard();
0291         return;
0292     }
0293     // If something went wrong fallback to clearing the entire clipboard
0294     if (!serviceJob->result().toBool()) {
0295         qCWarning(PLASMAPASS_LOG, "ServiceJob for clipboard failed internally, falling back to clearClipboard()");
0296         clearClipboard();
0297         return;
0298     }
0299 
0300     qCDebug(PLASMAPASS_LOG, "Successfully removed password from Klipper");
0301 }
0302 
0303 void ProviderBase::clearClipboard()
0304 {
0305     org::kde::klipper::klipper klipper(klipperDBusService, klipperDBusPath, QDBusConnection::sessionBus());
0306     if (!klipper.isValid()) {
0307         return;
0308     }
0309 
0310     klipper.clearClipboardHistory();
0311     klipper.clearClipboardContents();
0312 }
0313 
0314 #include "moc_providerbase.cpp"