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"