diff options
-rw-r--r-- | src/gui/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/gui/creds/keychainchunk.cpp | 219 | ||||
-rw-r--r-- | src/gui/creds/keychainchunk.h | 120 | ||||
-rw-r--r-- | src/gui/creds/webflowcredentials.cpp | 271 | ||||
-rw-r--r-- | src/gui/creds/webflowcredentials.h | 32 |
5 files changed, 450 insertions, 193 deletions
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 63618fc11..285057992 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -109,6 +109,7 @@ set(client_SRCS creds/httpcredentialsgui.cpp creds/oauth.cpp creds/flow2auth.cpp + creds/keychainchunk.cpp creds/webflowcredentials.cpp creds/webflowcredentialsdialog.cpp wizard/postfixlineedit.cpp diff --git a/src/gui/creds/keychainchunk.cpp b/src/gui/creds/keychainchunk.cpp new file mode 100644 index 000000000..4115c938d --- /dev/null +++ b/src/gui/creds/keychainchunk.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) by Michael Schuster <michael@nextcloud.com> + * + * 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) any later version. + * + * 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. + */ + +#include "account.h" +#include "keychainchunk.h" +#include "theme.h" +#include "networkjobs.h" +#include "configfile.h" +#include "creds/abstractcredentials.h" + +using namespace QKeychain; + +namespace OCC { + +Q_LOGGING_CATEGORY(lcKeychainChunk, "nextcloud.sync.credentials.keychainchunk", QtInfoMsg) + +namespace KeychainChunk { + +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) +static void addSettingsToJob(Account *account, QKeychain::Job *job) +{ + Q_UNUSED(account) + auto settings = ConfigFile::settingsWithGroup(Theme::instance()->appName()); + settings->setParent(job); // make the job parent to make setting deleted properly + job->setSettings(settings.release()); +} +#endif + +/* +* Job +*/ +Job::Job(QObject *parent) + : QObject(parent) +{ + _serviceName = Theme::instance()->appName(); +} + +/* +* WriteJob +*/ +WriteJob::WriteJob(Account *account, const QString &key, const QByteArray &data, QObject *parent) + : Job(parent) +{ + _account = account; + _key = key; + + // Windows workaround: Split the private key into chunks of 2048 bytes, + // to allow 4k (4096 bit) keys to be saved (obey Windows's limits) + _chunkBuffer = data; + _chunkCount = 0; +} + +void WriteJob::start() +{ + slotWriteJobDone(nullptr); +} + +void WriteJob::slotWriteJobDone(QKeychain::Job *incomingJob) +{ + QKeychain::WritePasswordJob *writeJob = static_cast<QKeychain::WritePasswordJob *>(incomingJob); + + // errors? + if (writeJob) { + _error = writeJob->error(); + _errorString = writeJob->errorString(); + + if (writeJob->error() != NoError) { + qCWarning(lcKeychainChunk) << "Error while writing" << writeJob->key() << "chunk" << writeJob->errorString(); + _chunkBuffer.clear(); + } + } + + // write a chunk if there is any in the buffer + if (!_chunkBuffer.isEmpty()) { +#if defined(Q_OS_WIN) + // Windows workaround: Split the data into chunks of 2048 bytes, + // to allow 4k (4096 bit) keys to be saved (obey Windows's limits) + auto chunk = _chunkBuffer.left(KeychainChunk::ChunkSize); + + _chunkBuffer = _chunkBuffer.right(_chunkBuffer.size() - chunk.size()); +#else + // write full data in one chunk on non-Windows, as usual + auto chunk = _chunkBuffer; + + _chunkBuffer.clear(); +#endif + auto index = (_chunkCount++); + + // keep the limit + if (_chunkCount > KeychainChunk::MaxChunks) { + qCWarning(lcKeychainChunk) << "Maximum chunk count exceeded while writing" << writeJob->key() << "chunk" << QString::number(index) << "cutting off after" << QString::number(KeychainChunk::MaxChunks) << "chunks"; + + writeJob->deleteLater(); + + _chunkBuffer.clear(); + + emit finished(this); + return; + } + + const QString kck = AbstractCredentials::keychainKey( + _account->url().toString(), + _key + (index > 0 ? (QString(".") + QString::number(index)) : QString()), + _account->id()); + + QKeychain::WritePasswordJob *job = new QKeychain::WritePasswordJob(_serviceName); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) + addSettingsToJob(_account, job); +#endif + job->setInsecureFallback(_insecureFallback); + connect(job, &QKeychain::Job::finished, this, &KeychainChunk::WriteJob::slotWriteJobDone); + // only add the key's (sub)"index" after the first element, to stay compatible with older versions and non-Windows + job->setKey(kck); + job->setBinaryData(chunk); + job->start(); + + chunk.clear(); + } else { + emit finished(this); + } + + writeJob->deleteLater(); +} + +/* +* ReadJob +*/ +ReadJob::ReadJob(Account *account, const QString &key, const bool &keychainMigration, QObject *parent) + : Job(parent) +{ + _account = account; + _key = key; + + _keychainMigration = keychainMigration; + + _chunkCount = 0; + _chunkBuffer.clear(); +} + +void ReadJob::start() +{ + _chunkCount = 0; + _chunkBuffer.clear(); + + const QString kck = AbstractCredentials::keychainKey( + _account->url().toString(), + _key, + _keychainMigration ? QString() : _account->id()); + + QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob(_serviceName); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) + addSettingsToJob(_account, job); +#endif + job->setInsecureFallback(_insecureFallback); + job->setKey(kck); + connect(job, &QKeychain::Job::finished, this, &KeychainChunk::ReadJob::slotReadJobDone); + job->start(); +} + +void ReadJob::slotReadJobDone(QKeychain::Job *incomingJob) +{ + // Errors or next chunk? + QKeychain::ReadPasswordJob *readJob = static_cast<QKeychain::ReadPasswordJob *>(incomingJob); + + if (readJob) { + _error = readJob->error(); + _errorString = readJob->errorString(); + + if (readJob->error() == NoError && readJob->binaryData().length() > 0) { + _chunkBuffer.append(readJob->binaryData()); + _chunkCount++; + +#if defined(Q_OS_WIN) + // try to fetch next chunk + if (_chunkCount < KeychainChunk::MaxChunks) { + const QString kck = AbstractCredentials::keychainKey( + _account->url().toString(), + _key + QString(".") + QString::number(_chunkCount), + _keychainMigration ? QString() : _account->id()); + + QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob(_serviceName); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) + addSettingsToJob(_account, job); +#endif + job->setInsecureFallback(_insecureFallback); + job->setKey(kck); + connect(job, &QKeychain::Job::finished, this, &KeychainChunk::ReadJob::slotReadJobDone); + job->start(); + + readJob->deleteLater(); + return; + } else { + qCWarning(lcKeychainChunk) << "Maximum chunk count for" << readJob->key() << "reached, ignoring after" << KeychainChunk::MaxChunks; + } +#endif + } else { + qCWarning(lcKeychainChunk) << "Unable to read" << readJob->key() << "chunk" << QString::number(_chunkCount) << readJob->errorString(); + } + + readJob->deleteLater(); + } + + emit finished(this); +} + +} // namespace KeychainChunk + +} // namespace OCC diff --git a/src/gui/creds/keychainchunk.h b/src/gui/creds/keychainchunk.h new file mode 100644 index 000000000..875ab5037 --- /dev/null +++ b/src/gui/creds/keychainchunk.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) by Michael Schuster <michael@nextcloud.com> + * + * 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) any later version. + * + * 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. + */ + +#pragma once +#ifndef KEYCHAINCHUNK_H +#define KEYCHAINCHUNK_H + +#include <QObject> +#include <keychain.h> +#include "accountfwd.h" + +// We don't support insecure fallback +// #define KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK + +namespace OCC { + +namespace KeychainChunk { + +/* +* Workaround for Windows: +* +* Split the keychain entry's data into chunks of 2048 bytes, +* to allow 4k (4096 bit) keys / large certs to be saved (see limits in webflowcredentials.h) +*/ +static constexpr int ChunkSize = 2048; +static constexpr int MaxChunks = 10; + +/* + * @brief: Abstract base class for KeychainChunk jobs. + */ +class Job : public QObject { + Q_OBJECT +public: + Job(QObject *parent = nullptr); + + const QKeychain::Error error() const { + return _error; + } + const QString errorString() const { + return _errorString; + } + + QByteArray binaryData() const { + return _chunkBuffer; + } + + const bool insecureFallback() const { + return _insecureFallback; + } + +// If we use it but don't support insecure fallback, give us nice compilation errors ;p +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) + void setInsecureFallback(const bool &insecureFallback) + { + _insecureFallback = insecureFallback; + } +#endif + +protected: + QString _serviceName; + Account *_account; + QString _key; + bool _insecureFallback = false; + bool _keychainMigration = false; + + QKeychain::Error _error = QKeychain::NoError; + QString _errorString; + + int _chunkCount = 0; + QByteArray _chunkBuffer; +}; // class Job + +/* +* @brief: Simple wrapper class for QKeychain::WritePasswordJob, splits too large keychain entry's data into chunks on Windows +*/ +class WriteJob : public KeychainChunk::Job { + Q_OBJECT +public: + WriteJob(Account *account, const QString &key, const QByteArray &data, QObject *parent = nullptr); + void start(); + +signals: + void finished(KeychainChunk::WriteJob *incomingJob); + +private slots: + void slotWriteJobDone(QKeychain::Job *incomingJob); +}; // class WriteJob + +/* +* @brief: Simple wrapper class for QKeychain::ReadPasswordJob, splits too large keychain entry's data into chunks on Windows +*/ +class ReadJob : public KeychainChunk::Job { + Q_OBJECT +public: + ReadJob(Account *account, const QString &key, const bool &keychainMigration, QObject *parent = nullptr); + void start(); + +signals: + void finished(KeychainChunk::ReadJob *incomingJob); + +private slots: + void slotReadJobDone(QKeychain::Job *incomingJob); +}; // class ReadJob + +} // namespace KeychainChunk + +} // namespace OCC + +#endif // KEYCHAINCHUNK_H diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index c82798abf..4993a0dd0 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -18,6 +18,7 @@ #include "theme.h" #include "wizard/webview.h" #include "webflowcredentialsdialog.h" +#include "keychainchunk.h" using namespace QKeychain; @@ -75,6 +76,7 @@ private: QPointer<const WebFlowCredentials> _cred; }; +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) static void addSettingsToJob(Account *account, QKeychain::Job *job) { Q_UNUSED(account) @@ -82,6 +84,7 @@ static void addSettingsToJob(Account *account, QKeychain::Job *job) settings->setParent(job); // make the job parent to make setting deleted properly job->setSettings(settings.release()); } +#endif WebFlowCredentials::WebFlowCredentials() : _ready(false) @@ -238,86 +241,32 @@ void WebFlowCredentials::persist() { // write cert if there is one if (!_clientSslCertificate.isNull()) { - WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone); - job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id())); - job->setBinaryData(_clientSslCertificate.toPem()); + auto *job = new KeychainChunk::WriteJob(_account, + _user + clientCertificatePEMC, + _clientSslCertificate.toPem()); + connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone); job->start(); } else { // no cert, just write credentials - slotWriteClientCertPEMJobDone(); + slotWriteClientCertPEMJobDone(nullptr); } } -void WebFlowCredentials::slotWriteClientCertPEMJobDone() +void WebFlowCredentials::slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob) { + if(writeJob) + writeJob->deleteLater(); + // write ssl key if there is one if (!_clientSslKey.isNull()) { - // Windows workaround: Split the private key into chunks of 2048 bytes, - // to allow 4k (4096 bit) keys to be saved (obey Windows's limits) - _clientSslKeyChunkBufferPEM = _clientSslKey.toPem(); - _clientSslKeyChunkCount = 0; - - writeSingleClientKeyChunkPEM(nullptr); - } else { - // no key, just write credentials - slotWriteClientKeyPEMJobDone(); - } -} - -void WebFlowCredentials::writeSingleClientKeyChunkPEM(QKeychain::Job *incomingJob) -{ - // errors? - if (incomingJob) { - WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob); - - if (writeJob->error() != NoError) { - qCWarning(lcWebFlowCredentials) << "Error while writing client CA key chunk" << writeJob->errorString(); - - _clientSslKeyChunkBufferPEM.clear(); - } - } - - // write a key chunk if there is any in the buffer - if (!_clientSslKeyChunkBufferPEM.isEmpty()) { -#if defined(Q_OS_WIN) - // Windows workaround: Split the private key into chunks of 2048 bytes, - // to allow 4k (4096 bit) keys to be saved (obey Windows's limits) - auto chunk = _clientSslKeyChunkBufferPEM.left(_clientSslKeyChunkSize); - - _clientSslKeyChunkBufferPEM = _clientSslKeyChunkBufferPEM.right(_clientSslKeyChunkBufferPEM.size() - chunk.size()); -#else - // write full key in one slot on non-Windows, as usual - auto chunk = _clientSslKeyChunkBufferPEM; - - _clientSslKeyChunkBufferPEM.clear(); -#endif - auto index = (_clientSslKeyChunkCount++); - - // keep the limit - if (_clientSslKeyChunkCount > _clientSslKeyMaxChunks) { - qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count exceeded while writing slot" << QString::number(index) << "cutting off after" << QString::number(_clientSslKeyMaxChunks) << "chunks"; - - _clientSslKeyChunkBufferPEM.clear(); - - slotWriteClientKeyPEMJobDone(); - return; - } - - WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - connect(job, &Job::finished, this, &WebFlowCredentials::writeSingleClientKeyChunkPEM); - // only add the key's (sub)"index" after the first element, to stay compatible with older versions and non-Windows - job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC + (index > 0 ? (QString(".") + QString::number(index)) : QString()), _account->id())); - job->setBinaryData(chunk); + auto *job = new KeychainChunk::WriteJob(_account, + _user + clientKeyPEMC, + _clientSslKey.toPem()); + connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone); job->start(); - - chunk.clear(); } else { - slotWriteClientKeyPEMJobDone(); + // no key, just write credentials + slotWriteClientKeyPEMJobDone(nullptr); } } @@ -340,20 +289,21 @@ void WebFlowCredentials::writeSingleClientCaCertPEM() return; } - WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone); - job->setKey(keychainKey(_account->url().toString(), _user + clientCaCertificatePEMC + QString::number(index), _account->id())); - job->setBinaryData(cert.toPem()); + auto *job = new KeychainChunk::WriteJob(_account, + _user + clientCaCertificatePEMC + QString::number(index), + cert.toPem()); + connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone); job->start(); } else { slotWriteClientCaCertsPEMJobDone(nullptr); } } -void WebFlowCredentials::slotWriteClientKeyPEMJobDone() +void WebFlowCredentials::slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob) { + if(writeJob) + writeJob->deleteLater(); + _clientSslCaCertificatesWriteQueue.clear(); // write ca certs if there are any @@ -368,16 +318,16 @@ void WebFlowCredentials::slotWriteClientKeyPEMJobDone() } } -void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob) +void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJob *writeJob) { // errors / next ca cert? - if (incomingJob && !_clientSslCaCertificates.isEmpty()) { - WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob); - + if (writeJob && !_clientSslCaCertificates.isEmpty()) { if (writeJob->error() != NoError) { qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString(); } + writeJob->deleteLater(); + if (!_clientSslCaCertificatesWriteQueue.isEmpty()) { // next ca cert writeSingleClientCaCertPEM(); @@ -387,7 +337,9 @@ void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomi // done storing ca certs, time for the password WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) addSettingsToJob(_account, job); +#endif job->setInsecureFallback(false); connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone); job->setKey(keychainKey(_account->url().toString(), _user, _account->id())); @@ -437,6 +389,10 @@ void WebFlowCredentials::forgetSensitiveData() { DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); job->setKey(kck); + connect(job, &Job::finished, this, [](QKeychain::Job *job) { + DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job); + djob->deleteLater(); + }); job->start(); invalidateToken(); @@ -487,29 +443,23 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) { void WebFlowCredentials::fetchFromKeychainHelper() { // Read client cert from keychain - const QString kck = keychainKey( - _account->url().toString(), - _user + clientCertificatePEMC, - _keychainMigration ? QString() : _account->id()); - - ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - job->setKey(kck); - connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone); + auto *job = new KeychainChunk::ReadJob(_account, + _user + clientCertificatePEMC, + _keychainMigration); + connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone); job->start(); } -void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob) +void WebFlowCredentials::slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob) { #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) - Q_ASSERT(!incomingJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless - if (_retryOnKeyChainError && (incomingJob->error() == QKeychain::NoBackendAvailable - || incomingJob->error() == QKeychain::OtherError)) { + Q_ASSERT(!readJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless + if (_retryOnKeyChainError && (readJob->error() == QKeychain::NoBackendAvailable + || readJob->error() == QKeychain::OtherError)) { // Could be that the backend was not yet available. Wait some extra seconds. // (Issues #4274 and #6522) // (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain) - qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << incomingJob->errorString(); + qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << readJob->errorString(); QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper); _retryOnKeyChainError = false; return; @@ -518,7 +468,6 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo #endif // Store PEM in memory - ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob); if (readJob->error() == NoError && readJob->binaryData().length() > 0) { QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem); if (sslCertificateList.length() >= 1) { @@ -526,79 +475,40 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo } } - // Load key too - _clientSslKeyChunkCount = 0; - _clientSslKeyChunkBufferPEM.clear(); + readJob->deleteLater(); - const QString kck = keychainKey( - _account->url().toString(), - _user + clientKeyPEMC, - _keychainMigration ? QString() : _account->id()); - - ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - job->setKey(kck); - connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone); + // Load key too + auto *job = new KeychainChunk::ReadJob(_account, + _user + clientKeyPEMC, + _keychainMigration); + connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone); job->start(); } -void WebFlowCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob) +void WebFlowCredentials::slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *readJob) { - // Errors or next key chunk? - ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob); - - if (readJob) { - if (readJob->error() == NoError && readJob->binaryData().length() > 0) { - _clientSslKeyChunkBufferPEM.append(readJob->binaryData()); - _clientSslKeyChunkCount++; - -#if defined(Q_OS_WIN) - // try to fetch next chunk - if (_clientSslKeyChunkCount < _clientSslKeyMaxChunks) { - const QString kck = keychainKey( - _account->url().toString(), - _user + clientKeyPEMC + QString(".") + QString::number(_clientSslKeyChunkCount), - _keychainMigration ? QString() : _account->id()); - - ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - job->setKey(kck); - connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone); - job->start(); - - return; - } else { - qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count reached, ignoring after" << _clientSslKeyMaxChunks; - } -#endif - } else { - if (readJob->error() != QKeychain::Error::EntryNotFound || - ((readJob->error() == QKeychain::Error::EntryNotFound) && _clientSslKeyChunkCount == 0)) { - qCWarning(lcWebFlowCredentials) << "Unable to read client key chunk slot" << QString::number(_clientSslKeyChunkCount) << readJob->errorString(); - } - } - } - // Store key in memory - if (_clientSslKeyChunkBufferPEM.size() > 0) { + if (readJob->error() == NoError && readJob->binaryData().length() > 0) { + QByteArray clientKeyPEM = readJob->binaryData(); // FIXME Unfortunately Qt has a bug and we can't just use QSsl::Opaque to let it // load whatever we have. So we try until it works. - _clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Rsa); + _clientSslKey = QSslKey(clientKeyPEM, QSsl::Rsa); if (_clientSslKey.isNull()) { - _clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Dsa); + _clientSslKey = QSslKey(clientKeyPEM, QSsl::Dsa); } if (_clientSslKey.isNull()) { - _clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Ec); + _clientSslKey = QSslKey(clientKeyPEM, QSsl::Ec); } if (_clientSslKey.isNull()) { qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!"; } - // clear key chunk buffer, but don't set _clientSslKeyChunkCount to zero because we need it for deleteKeychainEntries - _clientSslKeyChunkBufferPEM.clear(); + clientKeyPEM.clear(); + } else { + qCWarning(lcWebFlowCredentials) << "Unable to read client key" << readJob->errorString(); } + readJob->deleteLater(); + // Start fetching client CA certs _clientSslCaCertificates.clear(); @@ -609,16 +519,10 @@ void WebFlowCredentials::readSingleClientCaCertPEM() { // try to fetch a client ca cert if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) { - const QString kck = keychainKey( - _account->url().toString(), - _user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()), - _keychainMigration ? QString() : _account->id()); - - ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); - addSettingsToJob(_account, job); - job->setInsecureFallback(false); - job->setKey(kck); - connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone); + auto *job = new KeychainChunk::ReadJob(_account, + _user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()), + _keychainMigration); + connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone); job->start(); } else { qCWarning(lcWebFlowCredentials) << "Maximum client CA cert count exceeded while reading, ignoring after" << _clientSslCaCertificatesMaxCount; @@ -627,10 +531,8 @@ void WebFlowCredentials::readSingleClientCaCertPEM() } } -void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomingJob) { - // Store key in memory - ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob); - +void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob) { + // Store cert in memory if (readJob) { if (readJob->error() == NoError && readJob->binaryData().length() > 0) { QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem); @@ -638,6 +540,8 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin _clientSslCaCertificates.append(sslCertificateList.at(0)); } + readJob->deleteLater(); + // try next cert readSingleClientCaCertPEM(); return; @@ -647,6 +551,8 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin qCWarning(lcWebFlowCredentials) << "Unable to read client CA cert slot" << QString::number(_clientSslCaCertificates.count()) << readJob->errorString(); } } + + readJob->deleteLater(); } // Now fetch the actual server password @@ -656,7 +562,9 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin _keychainMigration ? QString() : _account->id()); ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) addSettingsToJob(_account, job); +#endif job->setInsecureFallback(false); job->setKey(kck); connect(job, &Job::finished, this, &WebFlowCredentials::slotReadPasswordJobDone); @@ -664,7 +572,7 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin } void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) { - QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob); + QKeychain::ReadPasswordJob *job = qobject_cast<ReadPasswordJob *>(incomingJob); QKeychain::Error error = job->error(); // If we could not find the entry try the old entries @@ -687,6 +595,8 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) { } emit fetched(); + job->deleteLater(); + // If keychain data was read from legacy location, wipe these entries and store new ones if (_keychainMigration && _ready) { _keychainMigration = false; @@ -697,13 +607,20 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) { } void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) { - auto startDeleteJob = [this, oldKeychainEntries](QString user) { + auto startDeleteJob = [this, oldKeychainEntries](QString key) { DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); +#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK) addSettingsToJob(_account, job); +#endif job->setInsecureFallback(false); job->setKey(keychainKey(_account->url().toString(), - user, + key, oldKeychainEntries ? QString() : _account->id())); + + connect(job, &Job::finished, this, [](QKeychain::Job *job) { + DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job); + djob->deleteLater(); + }); job->start(); }; @@ -728,9 +645,17 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) { } #if defined(Q_OS_WIN) - // also delete key sub-chunks (Windows workaround) - for (auto i = 1; i < _clientSslKeyChunkCount; i++) { - startDeleteJob(_user + clientKeyPEMC + QString(".") + QString::number(i)); + // Also delete key / cert sub-chunks (Windows workaround) + // The first chunk (0) has no suffix, to stay compatible with older versions and non-Windows + for (auto chunk = 1; chunk < KeychainChunk::MaxChunks; chunk++) { + const QString strChunkSuffix = QString(".") + QString::number(chunk); + + startDeleteJob(_user + clientKeyPEMC + strChunkSuffix); + startDeleteJob(_user + clientCertificatePEMC + strChunkSuffix); + + for (auto i = 0; i < _clientSslCaCertificates.count(); i++) { + startDeleteJob(_user + clientCaCertificatePEMC + QString::number(i)); + } } #endif // FIXME MS@2019-12-07 --> @@ -738,4 +663,4 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) { // <-- FIXME MS@2019-12-07 } -} +} // namespace OCC diff --git a/src/gui/creds/webflowcredentials.h b/src/gui/creds/webflowcredentials.h index 50c392ae6..4b2414b01 100644 --- a/src/gui/creds/webflowcredentials.h +++ b/src/gui/creds/webflowcredentials.h @@ -19,6 +19,11 @@ namespace QKeychain { namespace OCC { +namespace KeychainChunk { + class ReadJob; + class WriteJob; +} + class WebFlowCredentialsDialog; class WebFlowCredentials : public AbstractCredentials @@ -63,14 +68,14 @@ private slots: void slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host); void slotAskFromUserCancelled(); - void slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob); - void slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob); - void slotReadClientCaCertsPEMJobDone(QKeychain::Job *incommingJob); + void slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob); + void slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *readJob); + void slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob); void slotReadPasswordJobDone(QKeychain::Job *incomingJob); - void slotWriteClientCertPEMJobDone(); - void slotWriteClientKeyPEMJobDone(); - void slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob); + void slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob); + void slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob); + void slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJob *writeJob); void slotWriteJobDone(QKeychain::Job *); private: @@ -92,19 +97,6 @@ private: static constexpr int _clientSslCaCertificatesMaxCount = 10; QQueue<QSslCertificate> _clientSslCaCertificatesWriteQueue; - /* - * Workaround: ...and this time only on Windows: - * - * Split the private key into chunks of 2048 bytes, - * to allow 4k (4096 bit) keys to be saved (see limits above) - */ - void writeSingleClientKeyChunkPEM(QKeychain::Job *incomingJob); - - static constexpr int _clientSslKeyChunkSize = 2048; - static constexpr int _clientSslKeyMaxChunks = 10; - int _clientSslKeyChunkCount = 0; - QByteArray _clientSslKeyChunkBufferPEM; - protected: /** Reads data from keychain locations * @@ -135,6 +127,6 @@ protected: WebFlowCredentialsDialog *_askDialog; }; -} +} // namespace OCC #endif // WEBFLOWCREDENTIALS_H |