Heavy refactoring: Windows workaround for >= 4k (4096 bit) client-cert SSL keys and large certs

With QtKeychain on Windows, storing larger keys or certs in one keychain entry causes the
following error due to limits in the Windows APIs:
    Error: "Credential size exceeds maximum size of 2560"

This fix implements the new wrapper class KeychainChunk with wrapper jobs ReadJob and WriteJob
to encapsulate the QKeychain handling of ReadPasswordJob and WritePasswordJob with binaryData
but split every supplied keychain entry's data into 2048 byte chunks, on Windows only.

The wrapper is used for all keychain operations in WebFlowCredentials, except for the server password.

All finished keychain jobs now get deleted properly, to avoid memory leaks.

For reference also see previous fixes:
- https://github.com/nextcloud/desktop/pull/1389
- https://github.com/nextcloud/desktop/pull/1394

This should finally fix the re-opened issue:
- https://github.com/nextcloud/desktop/issues/863

Signed-off-by: Michael Schuster <michael@schuster.ms>
This commit is contained in:
Michael Schuster 2019-12-24 07:12:54 +01:00 committed by Michael Schuster
parent bd9652b24c
commit 9b034a2eb0
5 changed files with 450 additions and 193 deletions

View file

@ -109,6 +109,7 @@ set(client_SRCS
creds/httpcredentialsgui.cpp creds/httpcredentialsgui.cpp
creds/oauth.cpp creds/oauth.cpp
creds/flow2auth.cpp creds/flow2auth.cpp
creds/keychainchunk.cpp
creds/webflowcredentials.cpp creds/webflowcredentials.cpp
creds/webflowcredentialsdialog.cpp creds/webflowcredentialsdialog.cpp
wizard/postfixlineedit.cpp wizard/postfixlineedit.cpp

View file

@ -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

View file

@ -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

View file

@ -18,6 +18,7 @@
#include "theme.h" #include "theme.h"
#include "wizard/webview.h" #include "wizard/webview.h"
#include "webflowcredentialsdialog.h" #include "webflowcredentialsdialog.h"
#include "keychainchunk.h"
using namespace QKeychain; using namespace QKeychain;
@ -75,6 +76,7 @@ private:
QPointer<const WebFlowCredentials> _cred; QPointer<const WebFlowCredentials> _cred;
}; };
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
static void addSettingsToJob(Account *account, QKeychain::Job *job) static void addSettingsToJob(Account *account, QKeychain::Job *job)
{ {
Q_UNUSED(account) 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 settings->setParent(job); // make the job parent to make setting deleted properly
job->setSettings(settings.release()); job->setSettings(settings.release());
} }
#endif
WebFlowCredentials::WebFlowCredentials() WebFlowCredentials::WebFlowCredentials()
: _ready(false) : _ready(false)
@ -238,86 +241,32 @@ void WebFlowCredentials::persist() {
// write cert if there is one // write cert if there is one
if (!_clientSslCertificate.isNull()) { if (!_clientSslCertificate.isNull()) {
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); auto *job = new KeychainChunk::WriteJob(_account,
addSettingsToJob(_account, job); _user + clientCertificatePEMC,
job->setInsecureFallback(false); _clientSslCertificate.toPem());
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone); connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
job->setBinaryData(_clientSslCertificate.toPem());
job->start(); job->start();
} else { } else {
// no cert, just write credentials // 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 // write ssl key if there is one
if (!_clientSslKey.isNull()) { if (!_clientSslKey.isNull()) {
// Windows workaround: Split the private key into chunks of 2048 bytes, auto *job = new KeychainChunk::WriteJob(_account,
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits) _user + clientKeyPEMC,
_clientSslKeyChunkBufferPEM = _clientSslKey.toPem(); _clientSslKey.toPem());
_clientSslKeyChunkCount = 0; connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone);
job->start();
writeSingleClientKeyChunkPEM(nullptr);
} else { } else {
// no key, just write credentials // no key, just write credentials
slotWriteClientKeyPEMJobDone(); slotWriteClientKeyPEMJobDone(nullptr);
}
}
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);
job->start();
chunk.clear();
} else {
slotWriteClientKeyPEMJobDone();
} }
} }
@ -340,20 +289,21 @@ void WebFlowCredentials::writeSingleClientCaCertPEM()
return; return;
} }
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); auto *job = new KeychainChunk::WriteJob(_account,
addSettingsToJob(_account, job); _user + clientCaCertificatePEMC + QString::number(index),
job->setInsecureFallback(false); cert.toPem());
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone); connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientCaCertificatePEMC + QString::number(index), _account->id()));
job->setBinaryData(cert.toPem());
job->start(); job->start();
} else { } else {
slotWriteClientCaCertsPEMJobDone(nullptr); slotWriteClientCaCertsPEMJobDone(nullptr);
} }
} }
void WebFlowCredentials::slotWriteClientKeyPEMJobDone() void WebFlowCredentials::slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob)
{ {
if(writeJob)
writeJob->deleteLater();
_clientSslCaCertificatesWriteQueue.clear(); _clientSslCaCertificatesWriteQueue.clear();
// write ca certs if there are any // 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? // errors / next ca cert?
if (incomingJob && !_clientSslCaCertificates.isEmpty()) { if (writeJob && !_clientSslCaCertificates.isEmpty()) {
WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob);
if (writeJob->error() != NoError) { if (writeJob->error() != NoError) {
qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString(); qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString();
} }
writeJob->deleteLater();
if (!_clientSslCaCertificatesWriteQueue.isEmpty()) { if (!_clientSslCaCertificatesWriteQueue.isEmpty()) {
// next ca cert // next ca cert
writeSingleClientCaCertPEM(); writeSingleClientCaCertPEM();
@ -387,7 +337,9 @@ void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomi
// done storing ca certs, time for the password // done storing ca certs, time for the password
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job); addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false); job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone); connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone);
job->setKey(keychainKey(_account->url().toString(), _user, _account->id())); job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
@ -437,6 +389,10 @@ void WebFlowCredentials::forgetSensitiveData() {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false); job->setInsecureFallback(false);
job->setKey(kck); job->setKey(kck);
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job);
djob->deleteLater();
});
job->start(); job->start();
invalidateToken(); invalidateToken();
@ -487,29 +443,23 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
void WebFlowCredentials::fetchFromKeychainHelper() { void WebFlowCredentials::fetchFromKeychainHelper() {
// Read client cert from keychain // Read client cert from keychain
const QString kck = keychainKey( auto *job = new KeychainChunk::ReadJob(_account,
_account->url().toString(),
_user + clientCertificatePEMC, _user + clientCertificatePEMC,
_keychainMigration ? QString() : _account->id()); _keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
job->start(); job->start();
} }
void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob) void WebFlowCredentials::slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob)
{ {
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
Q_ASSERT(!incomingJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless Q_ASSERT(!readJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless
if (_retryOnKeyChainError && (incomingJob->error() == QKeychain::NoBackendAvailable if (_retryOnKeyChainError && (readJob->error() == QKeychain::NoBackendAvailable
|| incomingJob->error() == QKeychain::OtherError)) { || readJob->error() == QKeychain::OtherError)) {
// Could be that the backend was not yet available. Wait some extra seconds. // Could be that the backend was not yet available. Wait some extra seconds.
// (Issues #4274 and #6522) // (Issues #4274 and #6522)
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain) // (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); QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper);
_retryOnKeyChainError = false; _retryOnKeyChainError = false;
return; return;
@ -518,7 +468,6 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo
#endif #endif
// Store PEM in memory // Store PEM in memory
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
if (readJob->error() == NoError && readJob->binaryData().length() > 0) { if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem); QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
if (sslCertificateList.length() >= 1) { if (sslCertificateList.length() >= 1) {
@ -526,79 +475,40 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJo
} }
} }
readJob->deleteLater();
// Load key too // Load key too
_clientSslKeyChunkCount = 0; auto *job = new KeychainChunk::ReadJob(_account,
_clientSslKeyChunkBufferPEM.clear();
const QString kck = keychainKey(
_account->url().toString(),
_user + clientKeyPEMC, _user + clientKeyPEMC,
_keychainMigration ? QString() : _account->id()); _keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
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(); 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 // 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 // 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. // load whatever we have. So we try until it works.
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Rsa); _clientSslKey = QSslKey(clientKeyPEM, QSsl::Rsa);
if (_clientSslKey.isNull()) { if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Dsa); _clientSslKey = QSslKey(clientKeyPEM, QSsl::Dsa);
} }
if (_clientSslKey.isNull()) { if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Ec); _clientSslKey = QSslKey(clientKeyPEM, QSsl::Ec);
} }
if (_clientSslKey.isNull()) { if (_clientSslKey.isNull()) {
qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!"; 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 clientKeyPEM.clear();
_clientSslKeyChunkBufferPEM.clear(); } else {
qCWarning(lcWebFlowCredentials) << "Unable to read client key" << readJob->errorString();
} }
readJob->deleteLater();
// Start fetching client CA certs // Start fetching client CA certs
_clientSslCaCertificates.clear(); _clientSslCaCertificates.clear();
@ -609,16 +519,10 @@ void WebFlowCredentials::readSingleClientCaCertPEM()
{ {
// try to fetch a client ca cert // try to fetch a client ca cert
if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) { if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) {
const QString kck = keychainKey( auto *job = new KeychainChunk::ReadJob(_account,
_account->url().toString(),
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()), _user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
_keychainMigration ? QString() : _account->id()); _keychainMigration);
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
job->start(); job->start();
} else { } else {
qCWarning(lcWebFlowCredentials) << "Maximum client CA cert count exceeded while reading, ignoring after" << _clientSslCaCertificatesMaxCount; 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) { void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob) {
// Store key in memory // Store cert in memory
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
if (readJob) { if (readJob) {
if (readJob->error() == NoError && readJob->binaryData().length() > 0) { if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem); QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
@ -638,6 +540,8 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
_clientSslCaCertificates.append(sslCertificateList.at(0)); _clientSslCaCertificates.append(sslCertificateList.at(0));
} }
readJob->deleteLater();
// try next cert // try next cert
readSingleClientCaCertPEM(); readSingleClientCaCertPEM();
return; 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(); qCWarning(lcWebFlowCredentials) << "Unable to read client CA cert slot" << QString::number(_clientSslCaCertificates.count()) << readJob->errorString();
} }
} }
readJob->deleteLater();
} }
// Now fetch the actual server password // Now fetch the actual server password
@ -656,7 +562,9 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
_keychainMigration ? QString() : _account->id()); _keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName()); ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job); addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false); job->setInsecureFallback(false);
job->setKey(kck); job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadPasswordJobDone); connect(job, &Job::finished, this, &WebFlowCredentials::slotReadPasswordJobDone);
@ -664,7 +572,7 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomin
} }
void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) { void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob); QKeychain::ReadPasswordJob *job = qobject_cast<ReadPasswordJob *>(incomingJob);
QKeychain::Error error = job->error(); QKeychain::Error error = job->error();
// If we could not find the entry try the old entries // If we could not find the entry try the old entries
@ -687,6 +595,8 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
} }
emit fetched(); emit fetched();
job->deleteLater();
// If keychain data was read from legacy location, wipe these entries and store new ones // If keychain data was read from legacy location, wipe these entries and store new ones
if (_keychainMigration && _ready) { if (_keychainMigration && _ready) {
_keychainMigration = false; _keychainMigration = false;
@ -697,13 +607,20 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
} }
void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) { void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
auto startDeleteJob = [this, oldKeychainEntries](QString user) { auto startDeleteJob = [this, oldKeychainEntries](QString key) {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName()); DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
addSettingsToJob(_account, job); addSettingsToJob(_account, job);
#endif
job->setInsecureFallback(false); job->setInsecureFallback(false);
job->setKey(keychainKey(_account->url().toString(), job->setKey(keychainKey(_account->url().toString(),
user, key,
oldKeychainEntries ? QString() : _account->id())); oldKeychainEntries ? QString() : _account->id()));
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
DeletePasswordJob *djob = qobject_cast<DeletePasswordJob *>(job);
djob->deleteLater();
});
job->start(); job->start();
}; };
@ -728,9 +645,17 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
} }
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
// also delete key sub-chunks (Windows workaround) // Also delete key / cert sub-chunks (Windows workaround)
for (auto i = 1; i < _clientSslKeyChunkCount; i++) { // The first chunk (0) has no suffix, to stay compatible with older versions and non-Windows
startDeleteJob(_user + clientKeyPEMC + QString(".") + QString::number(i)); 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 #endif
// FIXME MS@2019-12-07 --> // FIXME MS@2019-12-07 -->
@ -738,4 +663,4 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
// <-- FIXME MS@2019-12-07 // <-- FIXME MS@2019-12-07
} }
} } // namespace OCC

View file

@ -19,6 +19,11 @@ namespace QKeychain {
namespace OCC { namespace OCC {
namespace KeychainChunk {
class ReadJob;
class WriteJob;
}
class WebFlowCredentialsDialog; class WebFlowCredentialsDialog;
class WebFlowCredentials : public AbstractCredentials class WebFlowCredentials : public AbstractCredentials
@ -63,14 +68,14 @@ private slots:
void slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host); void slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host);
void slotAskFromUserCancelled(); void slotAskFromUserCancelled();
void slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob); void slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob); void slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadClientCaCertsPEMJobDone(QKeychain::Job *incommingJob); void slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob *readJob);
void slotReadPasswordJobDone(QKeychain::Job *incomingJob); void slotReadPasswordJobDone(QKeychain::Job *incomingJob);
void slotWriteClientCertPEMJobDone(); void slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteClientKeyPEMJobDone(); void slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob); void slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJob *writeJob);
void slotWriteJobDone(QKeychain::Job *); void slotWriteJobDone(QKeychain::Job *);
private: private:
@ -92,19 +97,6 @@ private:
static constexpr int _clientSslCaCertificatesMaxCount = 10; static constexpr int _clientSslCaCertificatesMaxCount = 10;
QQueue<QSslCertificate> _clientSslCaCertificatesWriteQueue; 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: protected:
/** Reads data from keychain locations /** Reads data from keychain locations
* *
@ -135,6 +127,6 @@ protected:
WebFlowCredentialsDialog *_askDialog; WebFlowCredentialsDialog *_askDialog;
}; };
} } // namespace OCC
#endif // WEBFLOWCREDENTIALS_H #endif // WEBFLOWCREDENTIALS_H