mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-25 22:46:04 +03:00
Merge pull request #2127 from nextcloud/fix/proxy-password-storage
ConfigFile security: Migrate Proxy password to keychain
This commit is contained in:
commit
2475fb73b7
12 changed files with 807 additions and 472 deletions
|
@ -112,7 +112,6 @@ set(client_SRCS
|
|||
creds/httpcredentialsgui.cpp
|
||||
creds/oauth.cpp
|
||||
creds/flow2auth.cpp
|
||||
creds/keychainchunk.cpp
|
||||
creds/webflowcredentials.cpp
|
||||
creds/webflowcredentialsdialog.cpp
|
||||
wizard/postfixlineedit.cpp
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* 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 (readJob->error() != QKeychain::Error::EntryNotFound ||
|
||||
((readJob->error() == QKeychain::Error::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
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* 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
|
|
@ -1,13 +1,13 @@
|
|||
#include "webflowcredentials.h"
|
||||
|
||||
#include "creds/httpcredentials.h"
|
||||
#include "creds/keychainchunk.h"
|
||||
|
||||
#include <QAuthenticator>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <keychain.h>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
|
@ -18,7 +18,6 @@
|
|||
#include "theme.h"
|
||||
#include "wizard/webview.h"
|
||||
#include "webflowcredentialsdialog.h"
|
||||
#include "keychainchunk.h"
|
||||
|
||||
using namespace QKeychain;
|
||||
|
||||
|
@ -146,7 +145,7 @@ void WebFlowCredentials::fetchFromKeychain() {
|
|||
void WebFlowCredentials::askFromUser() {
|
||||
// Determine if the old flow has to be used (GS for now)
|
||||
// Do a DetermineAuthTypeJob to make sure that the server is still using Flow2
|
||||
auto *job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
||||
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
||||
connect(job, &DetermineAuthTypeJob::authType, [this](DetermineAuthTypeJob::AuthType type) {
|
||||
// LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic
|
||||
bool useFlow2 = (type != DetermineAuthTypeJob::WebViewFlow);
|
||||
|
@ -239,9 +238,10 @@ void WebFlowCredentials::persist() {
|
|||
|
||||
// write cert if there is one
|
||||
if (!_clientSslCertificate.isNull()) {
|
||||
auto *job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientCertificatePEMC,
|
||||
_clientSslCertificate.toPem());
|
||||
auto job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientCertificatePEMC,
|
||||
_clientSslCertificate.toPem(),
|
||||
this);
|
||||
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
|
||||
job->start();
|
||||
} else {
|
||||
|
@ -252,14 +252,12 @@ void WebFlowCredentials::persist() {
|
|||
|
||||
void WebFlowCredentials::slotWriteClientCertPEMJobDone(KeychainChunk::WriteJob *writeJob)
|
||||
{
|
||||
if(writeJob)
|
||||
writeJob->deleteLater();
|
||||
|
||||
// write ssl key if there is one
|
||||
if (!_clientSslKey.isNull()) {
|
||||
auto *job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientKeyPEMC,
|
||||
_clientSslKey.toPem());
|
||||
auto job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientKeyPEMC,
|
||||
_clientSslKey.toPem(),
|
||||
this);
|
||||
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone);
|
||||
job->start();
|
||||
} else {
|
||||
|
@ -287,9 +285,10 @@ void WebFlowCredentials::writeSingleClientCaCertPEM()
|
|||
return;
|
||||
}
|
||||
|
||||
auto *job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientCaCertificatePEMC + QString::number(index),
|
||||
cert.toPem());
|
||||
auto job = new KeychainChunk::WriteJob(_account,
|
||||
_user + clientCaCertificatePEMC + QString::number(index),
|
||||
cert.toPem(),
|
||||
this);
|
||||
connect(job, &KeychainChunk::WriteJob::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone);
|
||||
job->start();
|
||||
} else {
|
||||
|
@ -299,9 +298,6 @@ void WebFlowCredentials::writeSingleClientCaCertPEM()
|
|||
|
||||
void WebFlowCredentials::slotWriteClientKeyPEMJobDone(KeychainChunk::WriteJob *writeJob)
|
||||
{
|
||||
if(writeJob)
|
||||
writeJob->deleteLater();
|
||||
|
||||
_clientSslCaCertificatesWriteQueue.clear();
|
||||
|
||||
// write ca certs if there are any
|
||||
|
@ -324,8 +320,6 @@ void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJo
|
|||
qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString();
|
||||
}
|
||||
|
||||
writeJob->deleteLater();
|
||||
|
||||
if (!_clientSslCaCertificatesWriteQueue.isEmpty()) {
|
||||
// next ca cert
|
||||
writeSingleClientCaCertPEM();
|
||||
|
@ -334,7 +328,7 @@ void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(KeychainChunk::WriteJo
|
|||
}
|
||||
|
||||
// done storing ca certs, time for the password
|
||||
auto *job = new WritePasswordJob(Theme::instance()->appName());
|
||||
auto job = new WritePasswordJob(Theme::instance()->appName(), this);
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
addSettingsToJob(_account, job);
|
||||
#endif
|
||||
|
@ -354,8 +348,6 @@ void WebFlowCredentials::slotWriteJobDone(QKeychain::Job *job)
|
|||
default:
|
||||
qCWarning(lcWebFlowCredentials) << "Error while writing password" << job->errorString();
|
||||
}
|
||||
auto *wjob = qobject_cast<WritePasswordJob *>(job);
|
||||
wjob->deleteLater();
|
||||
}
|
||||
|
||||
void WebFlowCredentials::invalidateToken() {
|
||||
|
@ -384,13 +376,9 @@ void WebFlowCredentials::forgetSensitiveData() {
|
|||
return;
|
||||
}
|
||||
|
||||
auto *job = new DeletePasswordJob(Theme::instance()->appName());
|
||||
auto job = new DeletePasswordJob(Theme::instance()->appName(), this);
|
||||
job->setInsecureFallback(false);
|
||||
job->setKey(kck);
|
||||
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
|
||||
auto *djob = qobject_cast<DeletePasswordJob *>(job);
|
||||
djob->deleteLater();
|
||||
});
|
||||
job->start();
|
||||
|
||||
invalidateToken();
|
||||
|
@ -441,30 +429,16 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
|
|||
|
||||
void WebFlowCredentials::fetchFromKeychainHelper() {
|
||||
// Read client cert from keychain
|
||||
auto *job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientCertificatePEMC,
|
||||
_keychainMigration);
|
||||
auto job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientCertificatePEMC,
|
||||
_keychainMigration,
|
||||
this);
|
||||
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
|
||||
job->start();
|
||||
}
|
||||
|
||||
void WebFlowCredentials::slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *readJob)
|
||||
{
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
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." << readJob->errorString();
|
||||
QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper);
|
||||
_retryOnKeyChainError = false;
|
||||
return;
|
||||
}
|
||||
_retryOnKeyChainError = false;
|
||||
#endif
|
||||
|
||||
// Store PEM in memory
|
||||
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
||||
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
|
||||
|
@ -473,12 +447,11 @@ void WebFlowCredentials::slotReadClientCertPEMJobDone(KeychainChunk::ReadJob *re
|
|||
}
|
||||
}
|
||||
|
||||
readJob->deleteLater();
|
||||
|
||||
// Load key too
|
||||
auto *job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientKeyPEMC,
|
||||
_keychainMigration);
|
||||
auto job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientKeyPEMC,
|
||||
_keychainMigration,
|
||||
this);
|
||||
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
|
||||
job->start();
|
||||
}
|
||||
|
@ -505,8 +478,6 @@ void WebFlowCredentials::slotReadClientKeyPEMJobDone(KeychainChunk::ReadJob *rea
|
|||
qCWarning(lcWebFlowCredentials) << "Unable to read client key" << readJob->errorString();
|
||||
}
|
||||
|
||||
readJob->deleteLater();
|
||||
|
||||
// Start fetching client CA certs
|
||||
_clientSslCaCertificates.clear();
|
||||
|
||||
|
@ -517,9 +488,10 @@ void WebFlowCredentials::readSingleClientCaCertPEM()
|
|||
{
|
||||
// try to fetch a client ca cert
|
||||
if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) {
|
||||
auto *job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
|
||||
_keychainMigration);
|
||||
auto job = new KeychainChunk::ReadJob(_account,
|
||||
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
|
||||
_keychainMigration,
|
||||
this);
|
||||
connect(job, &KeychainChunk::ReadJob::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
|
||||
job->start();
|
||||
} else {
|
||||
|
@ -538,8 +510,6 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob
|
|||
_clientSslCaCertificates.append(sslCertificateList.at(0));
|
||||
}
|
||||
|
||||
readJob->deleteLater();
|
||||
|
||||
// try next cert
|
||||
readSingleClientCaCertPEM();
|
||||
return;
|
||||
|
@ -549,8 +519,6 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob
|
|||
qCWarning(lcWebFlowCredentials) << "Unable to read client CA cert slot" << QString::number(_clientSslCaCertificates.count()) << readJob->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
readJob->deleteLater();
|
||||
}
|
||||
|
||||
// Now fetch the actual server password
|
||||
|
@ -559,7 +527,7 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob
|
|||
_user,
|
||||
_keychainMigration ? QString() : _account->id());
|
||||
|
||||
auto *job = new ReadPasswordJob(Theme::instance()->appName());
|
||||
auto job = new ReadPasswordJob(Theme::instance()->appName(), this);
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
addSettingsToJob(_account, job);
|
||||
#endif
|
||||
|
@ -570,7 +538,7 @@ void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(KeychainChunk::ReadJob
|
|||
}
|
||||
|
||||
void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
|
||||
auto *job = qobject_cast<ReadPasswordJob *>(incomingJob);
|
||||
auto job = qobject_cast<ReadPasswordJob *>(incomingJob);
|
||||
QKeychain::Error error = job->error();
|
||||
|
||||
// If we could not find the entry try the old entries
|
||||
|
@ -593,8 +561,6 @@ 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;
|
||||
|
@ -606,19 +572,7 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
|
|||
|
||||
void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
|
||||
auto startDeleteJob = [this, oldKeychainEntries](QString key) {
|
||||
auto *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(),
|
||||
key,
|
||||
oldKeychainEntries ? QString() : _account->id()));
|
||||
|
||||
connect(job, &Job::finished, this, [](QKeychain::Job *job) {
|
||||
auto *djob = qobject_cast<DeletePasswordJob *>(job);
|
||||
djob->deleteLater();
|
||||
});
|
||||
auto job = new KeychainChunk::DeleteJob(_account, key, oldKeychainEntries, this);
|
||||
job->start();
|
||||
};
|
||||
|
||||
|
@ -635,27 +589,17 @@ void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
|
|||
*/
|
||||
if(_account->isRemoteWipeRequested_HACK()) {
|
||||
// <-- FIXME MS@2019-12-07
|
||||
|
||||
// Also delete key / cert sub-chunks (KeychainChunk takes care of the Windows workaround)
|
||||
// The first chunk (0) has no suffix, to stay compatible with older versions and non-Windows
|
||||
startDeleteJob(_user + clientKeyPEMC);
|
||||
startDeleteJob(_user + clientCertificatePEMC);
|
||||
|
||||
// CA cert slots
|
||||
for (auto i = 0; i < _clientSslCaCertificates.count(); i++) {
|
||||
startDeleteJob(_user + clientCaCertificatePEMC + QString::number(i));
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// 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 -->
|
||||
}
|
||||
// <-- FIXME MS@2019-12-07
|
||||
|
|
|
@ -122,7 +122,6 @@ protected:
|
|||
bool _ready = false;
|
||||
bool _credentialsValid = false;
|
||||
bool _keychainMigration = false;
|
||||
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
|
||||
|
||||
WebFlowCredentialsDialog *_askDialog = nullptr;
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <keychain.h>
|
||||
|
||||
using namespace OCC;
|
||||
using namespace QKeychain;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcProxy, "nextcloud.gui.credentials.proxy", QtInfoMsg)
|
||||
|
||||
|
@ -81,7 +82,7 @@ void ProxyAuthHandler::handleProxyAuthenticationRequired(
|
|||
// Find the responsible QNAM if possible.
|
||||
QNetworkAccessManager *sending_qnam = nullptr;
|
||||
QWeakPointer<QNetworkAccessManager> qnam_alive;
|
||||
if (auto *account = qobject_cast<Account *>(sender())) {
|
||||
if (auto account = qobject_cast<Account *>(sender())) {
|
||||
// Since we go into an event loop, it's possible for the account's qnam
|
||||
// to be destroyed before we get back. We can use this to check for its
|
||||
// liveness.
|
||||
|
@ -131,12 +132,7 @@ void ProxyAuthHandler::handleProxyAuthenticationRequired(
|
|||
this, &ProxyAuthHandler::slotSenderDestroyed);
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyAuthHandler::slotKeychainJobDone()
|
||||
{
|
||||
_keychainJobRunning = false;
|
||||
}
|
||||
|
||||
|
||||
void ProxyAuthHandler::slotSenderDestroyed(QObject *obj)
|
||||
{
|
||||
_gaveCredentialsTo.remove(obj);
|
||||
|
@ -154,11 +150,12 @@ bool ProxyAuthHandler::getCredsFromDialog()
|
|||
// This function can be reentered while the dialog is open.
|
||||
// If that's the case, continue processing the dialog until
|
||||
// it's done.
|
||||
++_waitingForDialog;
|
||||
while (_dialog && _dialog->isVisible()) {
|
||||
QApplication::processEvents(QEventLoop::ExcludeSocketNotifiers, 200);
|
||||
if(_dialog) {
|
||||
execAwait(_dialog.data(),
|
||||
&QDialog::finished,
|
||||
_waitingForDialog,
|
||||
QEventLoop::ExcludeSocketNotifiers);
|
||||
}
|
||||
--_waitingForDialog;
|
||||
|
||||
if (_dialog && _dialog->result() == QDialog::Accepted) {
|
||||
qCInfo(lcProxy) << "got creds for" << _proxy << "from dialog";
|
||||
|
@ -169,10 +166,26 @@ bool ProxyAuthHandler::getCredsFromDialog()
|
|||
return false;
|
||||
}
|
||||
|
||||
template<class T, typename PointerToMemberFunction>
|
||||
void ProxyAuthHandler::execAwait(const T *sender,
|
||||
PointerToMemberFunction signal,
|
||||
int &counter,
|
||||
const QEventLoop::ProcessEventsFlags flags)
|
||||
{
|
||||
if (!sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
QEventLoop waitLoop;
|
||||
connect(sender, signal, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
++counter;
|
||||
waitLoop.exec(flags);
|
||||
--counter;
|
||||
}
|
||||
|
||||
bool ProxyAuthHandler::getCredsFromKeychain()
|
||||
{
|
||||
using namespace QKeychain;
|
||||
|
||||
if (_waitingForDialog) {
|
||||
return false;
|
||||
}
|
||||
|
@ -190,9 +203,6 @@ bool ProxyAuthHandler::getCredsFromKeychain()
|
|||
_readPasswordJob->setInsecureFallback(false);
|
||||
_readPasswordJob->setKey(keychainPasswordKey());
|
||||
_readPasswordJob->setAutoDelete(false);
|
||||
connect(_readPasswordJob.data(), &QKeychain::Job::finished,
|
||||
this, &ProxyAuthHandler::slotKeychainJobDone);
|
||||
_keychainJobRunning = true;
|
||||
_readPasswordJob->start();
|
||||
}
|
||||
|
||||
|
@ -200,12 +210,9 @@ bool ProxyAuthHandler::getCredsFromKeychain()
|
|||
// This really needs the counter and the flag here, because otherwise we get
|
||||
// bad behavior when we reenter this code after the flag has been switched
|
||||
// but before the while loop has finished.
|
||||
++_waitingForKeychain;
|
||||
_keychainJobRunning = true;
|
||||
while (_keychainJobRunning) {
|
||||
QApplication::processEvents(QEventLoop::AllEvents, 200);
|
||||
}
|
||||
--_waitingForKeychain;
|
||||
execAwait(_readPasswordJob.data(),
|
||||
&QKeychain::Job::finished,
|
||||
_waitingForKeychain);
|
||||
|
||||
if (_readPasswordJob->error() == NoError) {
|
||||
qCInfo(lcProxy) << "got creds for" << _proxy << "from keychain";
|
||||
|
@ -222,8 +229,6 @@ bool ProxyAuthHandler::getCredsFromKeychain()
|
|||
|
||||
void ProxyAuthHandler::storeCredsInKeychain()
|
||||
{
|
||||
using namespace QKeychain;
|
||||
|
||||
if (_waitingForKeychain) {
|
||||
return;
|
||||
}
|
||||
|
@ -232,22 +237,17 @@ void ProxyAuthHandler::storeCredsInKeychain()
|
|||
|
||||
_settings->setValue(keychainUsernameKey(), _username);
|
||||
|
||||
auto *job = new WritePasswordJob(Theme::instance()->appName(), this);
|
||||
auto job = new WritePasswordJob(Theme::instance()->appName(), this);
|
||||
job->setSettings(_settings.data());
|
||||
job->setInsecureFallback(false);
|
||||
job->setKey(keychainPasswordKey());
|
||||
job->setTextData(_password);
|
||||
job->setAutoDelete(false);
|
||||
connect(job, &QKeychain::Job::finished, this, &ProxyAuthHandler::slotKeychainJobDone);
|
||||
_keychainJobRunning = true;
|
||||
job->start();
|
||||
|
||||
++_waitingForKeychain;
|
||||
_keychainJobRunning = true;
|
||||
while (_keychainJobRunning) {
|
||||
QApplication::processEvents(QEventLoop::AllEvents, 200);
|
||||
}
|
||||
--_waitingForKeychain;
|
||||
execAwait(job,
|
||||
&QKeychain::Job::finished,
|
||||
_waitingForKeychain);
|
||||
|
||||
job->deleteLater();
|
||||
if (job->error() != NoError) {
|
||||
|
|
|
@ -57,7 +57,6 @@ public slots:
|
|||
QAuthenticator *authenticator);
|
||||
|
||||
private slots:
|
||||
void slotKeychainJobDone();
|
||||
void slotSenderDestroyed(QObject *);
|
||||
|
||||
private:
|
||||
|
@ -72,6 +71,12 @@ private:
|
|||
/// Stores the current credentials in the keychain.
|
||||
void storeCredsInKeychain();
|
||||
|
||||
template<class T, typename PointerToMemberFunction>
|
||||
void execAwait(const T *sender,
|
||||
PointerToMemberFunction signal,
|
||||
int &counter,
|
||||
const QEventLoop::ProcessEventsFlags flags = QEventLoop::AllEvents);
|
||||
|
||||
QString keychainUsernameKey() const;
|
||||
QString keychainPasswordKey() const;
|
||||
|
||||
|
@ -91,7 +96,6 @@ private:
|
|||
/// waiting for.
|
||||
int _waitingForDialog = 0;
|
||||
int _waitingForKeychain = 0;
|
||||
bool _keychainJobRunning = false;
|
||||
|
||||
QPointer<ProxyAuthDialog> _dialog;
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ set(libsync_SRCS
|
|||
creds/dummycredentials.cpp
|
||||
creds/abstractcredentials.cpp
|
||||
creds/credentialscommon.cpp
|
||||
creds/keychainchunk.cpp
|
||||
)
|
||||
|
||||
if(TOKEN_AUTH_ONLY)
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "common/asserts.h"
|
||||
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "creds/keychainchunk.h"
|
||||
|
||||
#include "csync_exclude.h"
|
||||
|
||||
|
@ -651,7 +652,22 @@ void ConfigFile::setProxyType(int proxyType,
|
|||
settings.setValue(QLatin1String(proxyPortC), port);
|
||||
settings.setValue(QLatin1String(proxyNeedsAuthC), needsAuth);
|
||||
settings.setValue(QLatin1String(proxyUserC), user);
|
||||
settings.setValue(QLatin1String(proxyPassC), pass.toUtf8().toBase64());
|
||||
|
||||
if (pass.isEmpty()) {
|
||||
// Security: Don't keep password in config file
|
||||
settings.remove(QLatin1String(proxyPassC));
|
||||
|
||||
// Delete password from keychain
|
||||
auto job = new KeychainChunk::DeleteJob(keychainProxyPasswordKey());
|
||||
job->exec();
|
||||
} else {
|
||||
// Write password to keychain
|
||||
auto job = new KeychainChunk::WriteJob(keychainProxyPasswordKey(), pass.toUtf8());
|
||||
if (job->exec()) {
|
||||
// Security: Don't keep password in config file
|
||||
settings.remove(QLatin1String(proxyPassC));
|
||||
}
|
||||
}
|
||||
}
|
||||
settings.sync();
|
||||
}
|
||||
|
@ -726,8 +742,34 @@ QString ConfigFile::proxyUser() const
|
|||
|
||||
QString ConfigFile::proxyPassword() const
|
||||
{
|
||||
QByteArray pass = getValue(proxyPassC).toByteArray();
|
||||
return QString::fromUtf8(QByteArray::fromBase64(pass));
|
||||
QByteArray passEncoded = getValue(proxyPassC).toByteArray();
|
||||
auto pass = QString::fromUtf8(QByteArray::fromBase64(passEncoded));
|
||||
passEncoded.clear();
|
||||
|
||||
const auto key = keychainProxyPasswordKey();
|
||||
|
||||
if (!pass.isEmpty()) {
|
||||
// Security: Migrate password from config file to keychain
|
||||
auto job = new KeychainChunk::WriteJob(key, pass.toUtf8());
|
||||
if (job->exec()) {
|
||||
QSettings settings(configFile(), QSettings::IniFormat);
|
||||
settings.remove(QLatin1String(proxyPassC));
|
||||
qCInfo(lcConfigFile()) << "Migrated proxy password to keychain";
|
||||
}
|
||||
} else {
|
||||
// Read password from keychain
|
||||
auto job = new KeychainChunk::ReadJob(key);
|
||||
if (job->exec()) {
|
||||
pass = job->textData();
|
||||
}
|
||||
}
|
||||
|
||||
return pass;
|
||||
}
|
||||
|
||||
QString ConfigFile::keychainProxyPasswordKey() const
|
||||
{
|
||||
return QString::fromLatin1("proxy-password");
|
||||
}
|
||||
|
||||
int ConfigFile::useUploadLimit() const
|
||||
|
|
|
@ -198,6 +198,8 @@ private:
|
|||
const QVariant &defaultValue = QVariant()) const;
|
||||
void setValue(const QString &key, const QVariant &value);
|
||||
|
||||
QString keychainProxyPasswordKey() const;
|
||||
|
||||
private:
|
||||
typedef QSharedPointer<AbstractCredentials> SharedCreds;
|
||||
|
||||
|
|
487
src/libsync/creds/keychainchunk.cpp
Normal file
487
src/libsync/creds/keychainchunk.cpp
Normal file
|
@ -0,0 +1,487 @@
|
|||
/*
|
||||
* 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"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
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
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
void jobKeyPrependAppName(QString &key)
|
||||
{
|
||||
// NOTE: The following is normally done in AbstractCredentials::keychainKey
|
||||
// when an _account is specified by our other ctr overload (see 'kck' in this file).
|
||||
|
||||
// On Windows the credential keys aren't namespaced properly
|
||||
// by qtkeychain. To work around that we manually add namespacing
|
||||
// to the generated keys. See #6125.
|
||||
// It's safe to do that since the key format is changing for 2.4
|
||||
// anyway to include the account ids. That means old keys can be
|
||||
// migrated to new namespaced keys on windows for 2.4.
|
||||
key.prepend(QCoreApplication::applicationName() + "_");
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Job
|
||||
*/
|
||||
Job::Job(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
_serviceName = Theme::instance()->appName();
|
||||
}
|
||||
|
||||
Job::~Job()
|
||||
{
|
||||
_chunkCount = 0;
|
||||
_chunkBuffer.clear();
|
||||
}
|
||||
|
||||
QKeychain::Error Job::error() const
|
||||
{
|
||||
return _error;
|
||||
}
|
||||
|
||||
QString Job::errorString() const
|
||||
{
|
||||
return _errorString;
|
||||
}
|
||||
|
||||
QByteArray Job::binaryData() const
|
||||
{
|
||||
return _chunkBuffer;
|
||||
}
|
||||
|
||||
QString Job::textData() const
|
||||
{
|
||||
return _chunkBuffer;
|
||||
}
|
||||
|
||||
bool Job::insecureFallback() const
|
||||
{
|
||||
return _insecureFallback;
|
||||
}
|
||||
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
void Job::setInsecureFallback(bool insecureFallback)
|
||||
{
|
||||
_insecureFallback = insecureFallback;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Job::autoDelete() const
|
||||
{
|
||||
return _autoDelete;
|
||||
}
|
||||
|
||||
void Job::setAutoDelete(bool autoDelete)
|
||||
{
|
||||
_autoDelete = autoDelete;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
WriteJob::WriteJob(const QString &key, const QByteArray &data, QObject *parent)
|
||||
: WriteJob(nullptr, key, data, parent)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
jobKeyPrependAppName(_key);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WriteJob::start()
|
||||
{
|
||||
_error = QKeychain::NoError;
|
||||
|
||||
slotWriteJobDone(nullptr);
|
||||
}
|
||||
|
||||
bool WriteJob::exec()
|
||||
{
|
||||
start();
|
||||
|
||||
QEventLoop waitLoop;
|
||||
connect(this, &WriteJob::finished, &waitLoop, &QEventLoop::quit);
|
||||
waitLoop.exec();
|
||||
|
||||
if (error() != NoError) {
|
||||
qCWarning(lcKeychainChunk) << "WritePasswordJob failed with" << errorString();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WriteJob::slotWriteJobDone(QKeychain::Job *incomingJob)
|
||||
{
|
||||
auto writeJob = qobject_cast<QKeychain::WritePasswordJob *>(incomingJob);
|
||||
|
||||
// Errors? (writeJob can be nullptr here, see: WriteJob::start)
|
||||
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);
|
||||
|
||||
if (_autoDelete) {
|
||||
deleteLater();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const QString keyWithIndex = _key + (index > 0 ? (QString(".") + QString::number(index)) : QString());
|
||||
const QString kck = _account ? AbstractCredentials::keychainKey(
|
||||
_account->url().toString(),
|
||||
keyWithIndex,
|
||||
_account->id()
|
||||
) : keyWithIndex;
|
||||
|
||||
auto job = new QKeychain::WritePasswordJob(_serviceName, this);
|
||||
#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);
|
||||
|
||||
if (_autoDelete) {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
writeJob->deleteLater();
|
||||
}
|
||||
|
||||
/*
|
||||
* ReadJob
|
||||
*/
|
||||
ReadJob::ReadJob(Account *account, const QString &key, bool keychainMigration, QObject *parent)
|
||||
: Job(parent)
|
||||
{
|
||||
_account = account;
|
||||
_key = key;
|
||||
|
||||
_keychainMigration = keychainMigration;
|
||||
|
||||
_chunkCount = 0;
|
||||
_chunkBuffer.clear();
|
||||
}
|
||||
|
||||
ReadJob::ReadJob(const QString &key, QObject *parent)
|
||||
: ReadJob(nullptr, key, false, parent)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
jobKeyPrependAppName(_key);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ReadJob::start()
|
||||
{
|
||||
_chunkCount = 0;
|
||||
_chunkBuffer.clear();
|
||||
_error = QKeychain::NoError;
|
||||
|
||||
const QString kck = _account ? AbstractCredentials::keychainKey(
|
||||
_account->url().toString(),
|
||||
_key,
|
||||
_keychainMigration ? QString() : _account->id()
|
||||
) : _key;
|
||||
|
||||
auto job = new QKeychain::ReadPasswordJob(_serviceName, this);
|
||||
#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();
|
||||
}
|
||||
|
||||
bool ReadJob::exec()
|
||||
{
|
||||
start();
|
||||
|
||||
QEventLoop waitLoop;
|
||||
connect(this, &ReadJob::finished, &waitLoop, &QEventLoop::quit);
|
||||
waitLoop.exec();
|
||||
|
||||
if (error() == NoError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_chunkCount = 0;
|
||||
_chunkBuffer.clear();
|
||||
if (error() != EntryNotFound) {
|
||||
qCWarning(lcKeychainChunk) << "ReadPasswordJob failed with" << errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ReadJob::slotReadJobDone(QKeychain::Job *incomingJob)
|
||||
{
|
||||
// Errors or next chunk?
|
||||
auto readJob = qobject_cast<QKeychain::ReadPasswordJob *>(incomingJob);
|
||||
Q_ASSERT(readJob);
|
||||
|
||||
if (readJob->error() == NoError && !readJob->binaryData().isEmpty()) {
|
||||
_chunkBuffer.append(readJob->binaryData());
|
||||
_chunkCount++;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// try to fetch next chunk
|
||||
if (_chunkCount < KeychainChunk::MaxChunks) {
|
||||
const QString keyWithIndex = _key + QString(".") + QString::number(_chunkCount);
|
||||
const QString kck = _account ? AbstractCredentials::keychainKey(
|
||||
_account->url().toString(),
|
||||
keyWithIndex,
|
||||
_keychainMigration ? QString() : _account->id()
|
||||
) : keyWithIndex;
|
||||
|
||||
auto job = new QKeychain::ReadPasswordJob(_serviceName, this);
|
||||
#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);
|
||||
|
||||
if (_autoDelete) {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* DeleteJob
|
||||
*/
|
||||
DeleteJob::DeleteJob(Account *account, const QString &key, bool keychainMigration, QObject *parent)
|
||||
: Job(parent)
|
||||
{
|
||||
_account = account;
|
||||
_key = key;
|
||||
|
||||
_keychainMigration = keychainMigration;
|
||||
}
|
||||
|
||||
DeleteJob::DeleteJob(const QString &key, QObject *parent)
|
||||
: DeleteJob(nullptr, key, false, parent)
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
jobKeyPrependAppName(_key);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DeleteJob::start()
|
||||
{
|
||||
_chunkCount = 0;
|
||||
_error = QKeychain::NoError;
|
||||
|
||||
const QString kck = _account ? AbstractCredentials::keychainKey(
|
||||
_account->url().toString(),
|
||||
_key,
|
||||
_keychainMigration ? QString() : _account->id()
|
||||
) : _key;
|
||||
|
||||
auto job = new QKeychain::DeletePasswordJob(_serviceName, this);
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
addSettingsToJob(_account, job);
|
||||
#endif
|
||||
job->setInsecureFallback(_insecureFallback);
|
||||
job->setKey(kck);
|
||||
connect(job, &QKeychain::Job::finished, this, &KeychainChunk::DeleteJob::slotDeleteJobDone);
|
||||
job->start();
|
||||
}
|
||||
|
||||
bool DeleteJob::exec()
|
||||
{
|
||||
start();
|
||||
|
||||
QEventLoop waitLoop;
|
||||
connect(this, &DeleteJob::finished, &waitLoop, &QEventLoop::quit);
|
||||
waitLoop.exec();
|
||||
|
||||
if (error() == NoError) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_chunkCount = 0;
|
||||
if (error() != EntryNotFound) {
|
||||
qCWarning(lcKeychainChunk) << "DeletePasswordJob failed with" << errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DeleteJob::slotDeleteJobDone(QKeychain::Job *incomingJob)
|
||||
{
|
||||
// Errors or next chunk?
|
||||
auto deleteJob = qobject_cast<QKeychain::DeletePasswordJob *>(incomingJob);
|
||||
Q_ASSERT(deleteJob);
|
||||
|
||||
if (deleteJob->error() == NoError) {
|
||||
_chunkCount++;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// try to delete next chunk
|
||||
if (_chunkCount < KeychainChunk::MaxChunks) {
|
||||
const QString keyWithIndex = _key + QString(".") + QString::number(_chunkCount);
|
||||
const QString kck = _account ? AbstractCredentials::keychainKey(
|
||||
_account->url().toString(),
|
||||
keyWithIndex,
|
||||
_keychainMigration ? QString() : _account->id()
|
||||
) : keyWithIndex;
|
||||
|
||||
auto job = new QKeychain::DeletePasswordJob(_serviceName, this);
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
addSettingsToJob(_account, job);
|
||||
#endif
|
||||
job->setInsecureFallback(_insecureFallback);
|
||||
job->setKey(kck);
|
||||
connect(job, &QKeychain::Job::finished, this, &KeychainChunk::DeleteJob::slotDeleteJobDone);
|
||||
job->start();
|
||||
|
||||
deleteJob->deleteLater();
|
||||
return;
|
||||
} else {
|
||||
qCWarning(lcKeychainChunk) << "Maximum chunk count for" << deleteJob->key() << "reached, ignoring after" << KeychainChunk::MaxChunks;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
if (deleteJob->error() != QKeychain::EntryNotFound ||
|
||||
((deleteJob->error() == QKeychain::EntryNotFound) && _chunkCount == 0)) {
|
||||
_error = deleteJob->error();
|
||||
_errorString = deleteJob->errorString();
|
||||
qCWarning(lcKeychainChunk) << "Unable to delete" << deleteJob->key() << "chunk" << QString::number(_chunkCount) << deleteJob->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
deleteJob->deleteLater();
|
||||
|
||||
emit finished(this);
|
||||
|
||||
if (_autoDelete) {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace KeychainChunk
|
||||
|
||||
} // namespace OCC
|
198
src/libsync/creds/keychainchunk.h
Normal file
198
src/libsync/creds/keychainchunk.h
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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 OWNCLOUDSYNC_EXPORT Job : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
Job(QObject *parent = nullptr);
|
||||
|
||||
virtual ~Job();
|
||||
|
||||
QKeychain::Error error() const;
|
||||
QString errorString() const;
|
||||
|
||||
QByteArray binaryData() const;
|
||||
QString textData() const;
|
||||
|
||||
bool insecureFallback() const;
|
||||
|
||||
// If we use it but don't support insecure fallback, give us nice compilation errors ;p
|
||||
#if defined(KEYCHAINCHUNK_ENABLE_INSECURE_FALLBACK)
|
||||
void setInsecureFallback(bool insecureFallback);
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @return Whether this job autodeletes itself once finished() has been emitted. Default is true.
|
||||
* @see setAutoDelete()
|
||||
*/
|
||||
bool autoDelete() const;
|
||||
|
||||
/**
|
||||
* Set whether this job should autodelete itself once finished() has been emitted.
|
||||
* @see autoDelete()
|
||||
*/
|
||||
void setAutoDelete(bool autoDelete);
|
||||
|
||||
protected:
|
||||
QString _serviceName;
|
||||
Account *_account;
|
||||
QString _key;
|
||||
bool _insecureFallback = false;
|
||||
bool _autoDelete = true;
|
||||
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 OWNCLOUDSYNC_EXPORT WriteJob : public KeychainChunk::Job
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
WriteJob(Account *account, const QString &key, const QByteArray &data, QObject *parent = nullptr);
|
||||
WriteJob(const QString &key, const QByteArray &data, QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* Call this method to start the job (async).
|
||||
* You should connect some slot to the finished() signal first.
|
||||
*
|
||||
* @see QKeychain::Job::start()
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Call this method to start the job synchronously.
|
||||
* Awaits completion with no need to connect some slot to the finished() signal first.
|
||||
*
|
||||
* @return Returns true on succeess (QKeychain::NoError).
|
||||
*/
|
||||
bool exec();
|
||||
|
||||
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 OWNCLOUDSYNC_EXPORT ReadJob : public KeychainChunk::Job
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ReadJob(Account *account, const QString &key, bool keychainMigration, QObject *parent = nullptr);
|
||||
ReadJob(const QString &key, QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* Call this method to start the job (async).
|
||||
* You should connect some slot to the finished() signal first.
|
||||
*
|
||||
* @see QKeychain::Job::start()
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Call this method to start the job synchronously.
|
||||
* Awaits completion with no need to connect some slot to the finished() signal first.
|
||||
*
|
||||
* @return Returns true on succeess (QKeychain::NoError).
|
||||
*/
|
||||
bool exec();
|
||||
|
||||
signals:
|
||||
void finished(KeychainChunk::ReadJob *incomingJob);
|
||||
|
||||
private slots:
|
||||
void slotReadJobDone(QKeychain::Job *incomingJob);
|
||||
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
||||
private:
|
||||
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
|
||||
#endif
|
||||
}; // class ReadJob
|
||||
|
||||
/*
|
||||
* @brief: Simple wrapper class for QKeychain::DeletePasswordJob
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT DeleteJob : public KeychainChunk::Job
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DeleteJob(Account *account, const QString &key, bool keychainMigration, QObject *parent = nullptr);
|
||||
DeleteJob(const QString &key, QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* Call this method to start the job (async).
|
||||
* You should connect some slot to the finished() signal first.
|
||||
*
|
||||
* @see QKeychain::Job::start()
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Call this method to start the job synchronously.
|
||||
* Awaits completion with no need to connect some slot to the finished() signal first.
|
||||
*
|
||||
* @return Returns true on succeess (QKeychain::NoError).
|
||||
*/
|
||||
bool exec();
|
||||
|
||||
signals:
|
||||
void finished(KeychainChunk::DeleteJob *incomingJob);
|
||||
|
||||
private slots:
|
||||
void slotDeleteJobDone(QKeychain::Job *incomingJob);
|
||||
}; // class DeleteJob
|
||||
|
||||
} // namespace KeychainChunk
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif // KEYCHAINCHUNK_H
|
Loading…
Reference in a new issue