nextcloud-desktop/src/libsync/creds/keychainchunk.cpp
Michael Schuster 2a3ef044be
Move KeychainChunk class from gui to libsync
Signed-off-by: Michael Schuster <michael@schuster.ms>
2020-07-06 21:51:39 +02:00

239 lines
7.8 KiB
C++

/*
* 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)
{
auto *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());
auto *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());
auto *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?
auto *readJob = static_cast<QKeychain::ReadPasswordJob *>(incomingJob);
if (readJob) {
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 {
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
if (!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(lcKeychainChunk) << "Backend unavailable (yet?) Retrying in a few seconds." << readJob->errorString();
QTimer::singleShot(10000, this, &ReadJob::start);
_retryOnKeyChainError = false;
readJob->deleteLater();
return;
}
_retryOnKeyChainError = false;
}
#endif
if (readJob->error() != QKeychain::EntryNotFound ||
((readJob->error() == QKeychain::EntryNotFound) && _chunkCount == 0)) {
_error = readJob->error();
_errorString = readJob->errorString();
qCWarning(lcKeychainChunk) << "Unable to read" << readJob->key() << "chunk" << QString::number(_chunkCount) << readJob->errorString();
}
}
readJob->deleteLater();
}
emit finished(this);
}
} // namespace KeychainChunk
} // namespace OCC