mirror of
https://github.com/nextcloud/desktop.git
synced 2024-12-22 21:50:30 +03:00
5869b93acb
We never fall back to the insecure variant in the whole codebase, so don't do it here. Signed-off-by: Michael Schuster <michael@schuster.ms>
732 lines
26 KiB
C++
732 lines
26 KiB
C++
#include "webflowcredentials.h"
|
|
|
|
#include "creds/httpcredentials.h"
|
|
|
|
#include <QAuthenticator>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkReply>
|
|
#include <QPointer>
|
|
#include <QTimer>
|
|
#include <keychain.h>
|
|
#include <QDialog>
|
|
#include <QVBoxLayout>
|
|
#include <QLabel>
|
|
|
|
#include "accessmanager.h"
|
|
#include "account.h"
|
|
#include "configfile.h"
|
|
#include "theme.h"
|
|
#include "wizard/webview.h"
|
|
#include "webflowcredentialsdialog.h"
|
|
|
|
using namespace QKeychain;
|
|
|
|
namespace OCC {
|
|
|
|
Q_LOGGING_CATEGORY(lcWebFlowCredentials, "sync.credentials.webflow", QtInfoMsg)
|
|
|
|
namespace {
|
|
const char userC[] = "user";
|
|
const char clientCertificatePEMC[] = "_clientCertificatePEM";
|
|
const char clientKeyPEMC[] = "_clientKeyPEM";
|
|
const char clientCaCertificatePEMC[] = "_clientCaCertificatePEM";
|
|
} // ns
|
|
|
|
class WebFlowCredentialsAccessManager : public AccessManager
|
|
{
|
|
public:
|
|
WebFlowCredentialsAccessManager(const WebFlowCredentials *cred, QObject *parent = nullptr)
|
|
: AccessManager(parent)
|
|
, _cred(cred)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
|
|
{
|
|
QNetworkRequest req(request);
|
|
if (!req.attribute(WebFlowCredentials::DontAddCredentialsAttribute).toBool()) {
|
|
if (_cred && !_cred->password().isEmpty()) {
|
|
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
|
req.setRawHeader("Authorization", "Basic " + credHash);
|
|
}
|
|
}
|
|
|
|
if (_cred && !_cred->_clientSslKey.isNull() && !_cred->_clientSslCertificate.isNull()) {
|
|
// SSL configuration
|
|
QSslConfiguration sslConfiguration = req.sslConfiguration();
|
|
sslConfiguration.setLocalCertificate(_cred->_clientSslCertificate);
|
|
sslConfiguration.setPrivateKey(_cred->_clientSslKey);
|
|
|
|
// Merge client side CA with system CA
|
|
auto ca = sslConfiguration.systemCaCertificates();
|
|
ca.append(_cred->_clientSslCaCertificates);
|
|
sslConfiguration.setCaCertificates(ca);
|
|
|
|
req.setSslConfiguration(sslConfiguration);
|
|
}
|
|
|
|
return AccessManager::createRequest(op, req, outgoingData);
|
|
}
|
|
|
|
private:
|
|
// The credentials object dies along with the account, while the QNAM might
|
|
// outlive both.
|
|
QPointer<const WebFlowCredentials> _cred;
|
|
};
|
|
|
|
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());
|
|
}
|
|
|
|
WebFlowCredentials::WebFlowCredentials()
|
|
: _ready(false)
|
|
, _credentialsValid(false)
|
|
, _keychainMigration(false)
|
|
, _retryOnKeyChainError(false)
|
|
{
|
|
|
|
}
|
|
|
|
WebFlowCredentials::WebFlowCredentials(const QString &user, const QString &password, const QSslCertificate &certificate, const QSslKey &key, const QList<QSslCertificate> &caCertificates)
|
|
: _user(user)
|
|
, _password(password)
|
|
, _clientSslKey(key)
|
|
, _clientSslCertificate(certificate)
|
|
, _clientSslCaCertificates(caCertificates)
|
|
, _ready(true)
|
|
, _credentialsValid(true)
|
|
, _keychainMigration(false)
|
|
, _retryOnKeyChainError(false)
|
|
{
|
|
|
|
}
|
|
|
|
QString WebFlowCredentials::authType() const {
|
|
return QString::fromLatin1("webflow");
|
|
}
|
|
|
|
QString WebFlowCredentials::user() const {
|
|
return _user;
|
|
}
|
|
|
|
QString WebFlowCredentials::password() const {
|
|
return _password;
|
|
}
|
|
|
|
QNetworkAccessManager *WebFlowCredentials::createQNAM() const {
|
|
qCInfo(lcWebFlowCredentials()) << "Get QNAM";
|
|
AccessManager *qnam = new WebFlowCredentialsAccessManager(this);
|
|
|
|
connect(qnam, &AccessManager::authenticationRequired, this, &WebFlowCredentials::slotAuthentication);
|
|
connect(qnam, &AccessManager::finished, this, &WebFlowCredentials::slotFinished);
|
|
|
|
return qnam;
|
|
}
|
|
|
|
bool WebFlowCredentials::ready() const {
|
|
return _ready;
|
|
}
|
|
|
|
void WebFlowCredentials::fetchFromKeychain() {
|
|
_wasFetched = true;
|
|
|
|
// Make sure we get the user from the config file
|
|
fetchUser();
|
|
|
|
if (ready()) {
|
|
emit fetched();
|
|
} else {
|
|
qCInfo(lcWebFlowCredentials()) << "Fetch from keychain!";
|
|
fetchFromKeychainHelper();
|
|
}
|
|
}
|
|
|
|
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
|
|
DetermineAuthTypeJob *job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
|
connect(job, &DetermineAuthTypeJob::authType, [this](DetermineAuthTypeJob::AuthType type) {
|
|
// LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic
|
|
bool useFlow2 = (type != DetermineAuthTypeJob::WebViewFlow);
|
|
|
|
_askDialog = new WebFlowCredentialsDialog(_account, useFlow2);
|
|
|
|
if (!useFlow2) {
|
|
QUrl url = _account->url();
|
|
QString path = url.path() + "/index.php/login/flow";
|
|
url.setPath(path);
|
|
_askDialog->setUrl(url);
|
|
}
|
|
|
|
QString msg = tr("You have been logged out of %1 as user %2. Please login again")
|
|
.arg(_account->displayName(), _user);
|
|
_askDialog->setInfo(msg);
|
|
|
|
_askDialog->show();
|
|
|
|
connect(_askDialog, &WebFlowCredentialsDialog::urlCatched, this, &WebFlowCredentials::slotAskFromUserCredentialsProvided);
|
|
});
|
|
job->start();
|
|
|
|
qCDebug(lcWebFlowCredentials()) << "User needs to reauth!";
|
|
}
|
|
|
|
void WebFlowCredentials::slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host) {
|
|
Q_UNUSED(host)
|
|
|
|
if (_user != user) {
|
|
qCInfo(lcWebFlowCredentials()) << "Authed with the wrong user!";
|
|
|
|
QString msg = tr("Please login with the user: %1")
|
|
.arg(_user);
|
|
_askDialog->setError(msg);
|
|
|
|
if (!_askDialog->isUsingFlow2()) {
|
|
QUrl url = _account->url();
|
|
QString path = url.path() + "/index.php/login/flow";
|
|
url.setPath(path);
|
|
_askDialog->setUrl(url);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcWebFlowCredentials()) << "Obtained a new password";
|
|
|
|
_password = pass;
|
|
_ready = true;
|
|
_credentialsValid = true;
|
|
persist();
|
|
emit asked();
|
|
|
|
_askDialog->close();
|
|
delete _askDialog;
|
|
_askDialog = nullptr;
|
|
}
|
|
|
|
|
|
bool WebFlowCredentials::stillValid(QNetworkReply *reply) {
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
qCWarning(lcWebFlowCredentials()) << reply->error();
|
|
qCWarning(lcWebFlowCredentials()) << reply->errorString();
|
|
}
|
|
return (reply->error() != QNetworkReply::AuthenticationRequiredError);
|
|
}
|
|
|
|
void WebFlowCredentials::persist() {
|
|
if (_user.isEmpty()) {
|
|
// We don't even have a user nothing to see here move along
|
|
return;
|
|
}
|
|
|
|
_account->setCredentialSetting(userC, _user);
|
|
_account->wantsAccountSaved(_account);
|
|
|
|
// write cert if there is one
|
|
if (!_clientSslCertificate.isNull()) {
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCertPEMJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
|
|
job->setBinaryData(_clientSslCertificate.toPem());
|
|
job->start();
|
|
} else {
|
|
// no cert, just write credentials
|
|
slotWriteClientCertPEMJobDone();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteClientCertPEMJobDone()
|
|
{
|
|
// write ssl key if there is one
|
|
if (!_clientSslKey.isNull()) {
|
|
// Windows workaround: Split the private key into chunks of 2048 bytes,
|
|
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
|
|
_clientSslKeyChunkBufferPEM = _clientSslKey.toPem();
|
|
_clientSslKeyChunkCount = 0;
|
|
|
|
writeSingleClientKeyChunkPEM(nullptr);
|
|
} else {
|
|
// no key, just write credentials
|
|
slotWriteClientKeyPEMJobDone();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::writeSingleClientKeyChunkPEM(QKeychain::Job *incomingJob)
|
|
{
|
|
// errors?
|
|
if (incomingJob) {
|
|
WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob);
|
|
|
|
if (writeJob->error() != NoError) {
|
|
qCWarning(lcWebFlowCredentials) << "Error while writing client CA key chunk" << writeJob->errorString();
|
|
|
|
_clientSslKeyChunkBufferPEM.clear();
|
|
}
|
|
}
|
|
|
|
// write a key chunk if there is any in the buffer
|
|
if (!_clientSslKeyChunkBufferPEM.isEmpty()) {
|
|
#if defined(Q_OS_WIN)
|
|
// Windows workaround: Split the private key into chunks of 2048 bytes,
|
|
// to allow 4k (4096 bit) keys to be saved (obey Windows's limits)
|
|
auto chunk = _clientSslKeyChunkBufferPEM.left(_clientSslKeyChunkSize);
|
|
|
|
_clientSslKeyChunkBufferPEM = _clientSslKeyChunkBufferPEM.right(_clientSslKeyChunkBufferPEM.size() - chunk.size());
|
|
#else
|
|
// write full key in one slot on non-Windows, as usual
|
|
auto chunk = _clientSslKeyChunkBufferPEM;
|
|
|
|
_clientSslKeyChunkBufferPEM.clear();
|
|
#endif
|
|
auto index = (_clientSslKeyChunkCount++);
|
|
|
|
// keep the limit
|
|
if (_clientSslKeyChunkCount > _clientSslKeyMaxChunks) {
|
|
qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count exceeded while writing slot" << QString::number(index) << "cutting off after" << QString::number(_clientSslKeyMaxChunks) << "chunks";
|
|
|
|
_clientSslKeyChunkBufferPEM.clear();
|
|
|
|
slotWriteClientKeyPEMJobDone();
|
|
return;
|
|
}
|
|
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::writeSingleClientKeyChunkPEM);
|
|
// only add the key's (sub)"index" after the first element, to stay compatible with older versions and non-Windows
|
|
job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC + (index > 0 ? (QString(".") + QString::number(index)) : QString()), _account->id()));
|
|
job->setBinaryData(chunk);
|
|
job->start();
|
|
|
|
chunk.clear();
|
|
} else {
|
|
slotWriteClientKeyPEMJobDone();
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::writeSingleClientCaCertPEM()
|
|
{
|
|
// write a ca cert if there is any in the queue
|
|
if (!_clientSslCaCertificatesWriteQueue.isEmpty()) {
|
|
// grab and remove the first cert from the queue
|
|
auto cert = _clientSslCaCertificatesWriteQueue.dequeue();
|
|
|
|
auto index = (_clientSslCaCertificates.count() - _clientSslCaCertificatesWriteQueue.count()) - 1;
|
|
|
|
// keep the limit
|
|
if (index > (_clientSslCaCertificatesMaxCount - 1)) {
|
|
qCWarning(lcWebFlowCredentials) << "Maximum client CA cert count exceeded while writing slot" << QString::number(index) << "cutting off after" << QString::number(_clientSslCaCertificatesMaxCount) << "certs";
|
|
|
|
_clientSslCaCertificatesWriteQueue.clear();
|
|
|
|
slotWriteClientCaCertsPEMJobDone(nullptr);
|
|
return;
|
|
}
|
|
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientCaCertsPEMJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user + clientCaCertificatePEMC + QString::number(index), _account->id()));
|
|
job->setBinaryData(cert.toPem());
|
|
job->start();
|
|
} else {
|
|
slotWriteClientCaCertsPEMJobDone(nullptr);
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteClientKeyPEMJobDone()
|
|
{
|
|
_clientSslCaCertificatesWriteQueue.clear();
|
|
|
|
// write ca certs if there are any
|
|
if (!_clientSslCaCertificates.isEmpty()) {
|
|
// queue the certs to avoid trouble on Windows (Workaround for CredWriteW used by QtKeychain)
|
|
_clientSslCaCertificatesWriteQueue.append(_clientSslCaCertificates);
|
|
|
|
// first ca cert
|
|
writeSingleClientCaCertPEM();
|
|
} else {
|
|
slotWriteClientCaCertsPEMJobDone(nullptr);
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteClientCaCertsPEMJobDone(QKeychain::Job *incomingJob)
|
|
{
|
|
// errors / next ca cert?
|
|
if (incomingJob && !_clientSslCaCertificates.isEmpty()) {
|
|
WritePasswordJob *writeJob = static_cast<WritePasswordJob *>(incomingJob);
|
|
|
|
if (writeJob->error() != NoError) {
|
|
qCWarning(lcWebFlowCredentials) << "Error while writing client CA cert" << writeJob->errorString();
|
|
}
|
|
|
|
if (!_clientSslCaCertificatesWriteQueue.isEmpty()) {
|
|
// next ca cert
|
|
writeSingleClientCaCertPEM();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// done storing ca certs, time for the password
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteJobDone);
|
|
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
|
|
job->setTextData(_password);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotWriteJobDone(QKeychain::Job *job)
|
|
{
|
|
delete job->settings();
|
|
switch (job->error()) {
|
|
case NoError:
|
|
break;
|
|
default:
|
|
qCWarning(lcWebFlowCredentials) << "Error while writing password" << job->errorString();
|
|
}
|
|
WritePasswordJob *wjob = qobject_cast<WritePasswordJob *>(job);
|
|
wjob->deleteLater();
|
|
}
|
|
|
|
void WebFlowCredentials::invalidateToken() {
|
|
// clear the session cookie.
|
|
_account->clearCookieJar();
|
|
|
|
// let QNAM forget about the password
|
|
// This needs to be done later in the event loop because we might be called (directly or
|
|
// indirectly) from QNetworkAccessManagerPrivate::authenticationRequired, which itself
|
|
// is a called from a BlockingQueuedConnection from the Qt HTTP thread. And clearing the
|
|
// cache needs to synchronize again with the HTTP thread.
|
|
QTimer::singleShot(0, _account, &Account::clearQNAMCache);
|
|
}
|
|
|
|
void WebFlowCredentials::forgetSensitiveData() {
|
|
_password = QString();
|
|
_ready = false;
|
|
|
|
fetchUser();
|
|
|
|
_account->deleteAppPassword();
|
|
|
|
const QString kck = keychainKey(_account->url().toString(), _user, _account->id());
|
|
if (kck.isEmpty()) {
|
|
qCDebug(lcWebFlowCredentials()) << "InvalidateToken: User is empty, bailing out!";
|
|
return;
|
|
}
|
|
|
|
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
job->start();
|
|
|
|
invalidateToken();
|
|
|
|
deleteKeychainEntries();
|
|
}
|
|
|
|
void WebFlowCredentials::setAccount(Account *account) {
|
|
AbstractCredentials::setAccount(account);
|
|
if (_user.isEmpty()) {
|
|
fetchUser();
|
|
}
|
|
}
|
|
|
|
QString WebFlowCredentials::fetchUser() {
|
|
_user = _account->credentialSetting(userC).toString();
|
|
return _user;
|
|
}
|
|
|
|
void WebFlowCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator) {
|
|
Q_UNUSED(reply)
|
|
|
|
if (!_ready) {
|
|
return;
|
|
}
|
|
|
|
if (_credentialsValid == false) {
|
|
return;
|
|
}
|
|
|
|
qCDebug(lcWebFlowCredentials()) << "Requires authentication";
|
|
|
|
authenticator->setUser(_user);
|
|
authenticator->setPassword(_password);
|
|
_credentialsValid = false;
|
|
}
|
|
|
|
void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
|
|
qCInfo(lcWebFlowCredentials()) << "request finished";
|
|
|
|
if (reply->error() == QNetworkReply::NoError) {
|
|
_credentialsValid = true;
|
|
|
|
/// Used later for remote wipe
|
|
_account->writeAppPasswordOnce(_password);
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::fetchFromKeychainHelper() {
|
|
// Read client cert from keychain
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientCertificatePEMC,
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob)
|
|
{
|
|
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
|
|
Q_ASSERT(!incomingJob->insecureFallback()); // If insecureFallback is set, the next test would be pointless
|
|
if (_retryOnKeyChainError && (incomingJob->error() == QKeychain::NoBackendAvailable
|
|
|| incomingJob->error() == QKeychain::OtherError)) {
|
|
// Could be that the backend was not yet available. Wait some extra seconds.
|
|
// (Issues #4274 and #6522)
|
|
// (For kwallet, the error is OtherError instead of NoBackendAvailable, maybe a bug in QtKeychain)
|
|
qCInfo(lcWebFlowCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << incomingJob->errorString();
|
|
QTimer::singleShot(10000, this, &WebFlowCredentials::fetchFromKeychainHelper);
|
|
_retryOnKeyChainError = false;
|
|
return;
|
|
}
|
|
_retryOnKeyChainError = false;
|
|
#endif
|
|
|
|
// Store PEM in memory
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
|
|
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
|
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
|
|
if (sslCertificateList.length() >= 1) {
|
|
_clientSslCertificate = sslCertificateList.at(0);
|
|
}
|
|
}
|
|
|
|
// Load key too
|
|
_clientSslKeyChunkCount = 0;
|
|
_clientSslKeyChunkBufferPEM.clear();
|
|
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientKeyPEMC,
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob)
|
|
{
|
|
// Errors or next key chunk?
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
|
|
|
|
if (readJob) {
|
|
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
|
_clientSslKeyChunkBufferPEM.append(readJob->binaryData());
|
|
_clientSslKeyChunkCount++;
|
|
|
|
#if defined(Q_OS_WIN)
|
|
// try to fetch next chunk
|
|
if (_clientSslKeyChunkCount < _clientSslKeyMaxChunks) {
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientKeyPEMC + QString(".") + QString::number(_clientSslKeyChunkCount),
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientKeyPEMJobDone);
|
|
job->start();
|
|
|
|
return;
|
|
} else {
|
|
qCWarning(lcWebFlowCredentials) << "Maximum client key chunk count reached, ignoring after" << _clientSslKeyMaxChunks;
|
|
}
|
|
#endif
|
|
} else {
|
|
if (readJob->error() != QKeychain::Error::EntryNotFound ||
|
|
((readJob->error() == QKeychain::Error::EntryNotFound) && _clientSslKeyChunkCount == 0)) {
|
|
qCWarning(lcWebFlowCredentials) << "Unable to read client key chunk slot" << QString::number(_clientSslKeyChunkCount) << readJob->errorString();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store key in memory
|
|
if (_clientSslKeyChunkBufferPEM.size() > 0) {
|
|
// FIXME Unfortunately Qt has a bug and we can't just use QSsl::Opaque to let it
|
|
// load whatever we have. So we try until it works.
|
|
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Rsa);
|
|
if (_clientSslKey.isNull()) {
|
|
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Dsa);
|
|
}
|
|
if (_clientSslKey.isNull()) {
|
|
_clientSslKey = QSslKey(_clientSslKeyChunkBufferPEM, QSsl::Ec);
|
|
}
|
|
if (_clientSslKey.isNull()) {
|
|
qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!";
|
|
}
|
|
// clear key chunk buffer, but don't set _clientSslKeyChunkCount to zero because we need it for deleteKeychainEntries
|
|
_clientSslKeyChunkBufferPEM.clear();
|
|
}
|
|
|
|
// Start fetching client CA certs
|
|
_clientSslCaCertificates.clear();
|
|
|
|
readSingleClientCaCertPEM();
|
|
}
|
|
|
|
void WebFlowCredentials::readSingleClientCaCertPEM()
|
|
{
|
|
// try to fetch a client ca cert
|
|
if (_clientSslCaCertificates.count() < _clientSslCaCertificatesMaxCount) {
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user + clientCaCertificatePEMC + QString::number(_clientSslCaCertificates.count()),
|
|
_keychainMigration ? QString() : _account->id());
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCaCertsPEMJobDone);
|
|
job->start();
|
|
} else {
|
|
qCWarning(lcWebFlowCredentials) << "Maximum client CA cert count exceeded while reading, ignoring after" << _clientSslCaCertificatesMaxCount;
|
|
|
|
slotReadClientCaCertsPEMJobDone(nullptr);
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadClientCaCertsPEMJobDone(QKeychain::Job *incomingJob) {
|
|
// Store key in memory
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
|
|
|
|
if (readJob) {
|
|
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
|
|
QList<QSslCertificate> sslCertificateList = QSslCertificate::fromData(readJob->binaryData(), QSsl::Pem);
|
|
if (sslCertificateList.length() >= 1) {
|
|
_clientSslCaCertificates.append(sslCertificateList.at(0));
|
|
}
|
|
|
|
// try next cert
|
|
readSingleClientCaCertPEM();
|
|
return;
|
|
} else {
|
|
if (readJob->error() != QKeychain::Error::EntryNotFound ||
|
|
((readJob->error() == QKeychain::Error::EntryNotFound) && _clientSslCaCertificates.count() == 0)) {
|
|
qCWarning(lcWebFlowCredentials) << "Unable to read client CA cert slot" << QString::number(_clientSslCaCertificates.count()) << readJob->errorString();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now fetch the actual server password
|
|
const QString kck = keychainKey(
|
|
_account->url().toString(),
|
|
_user,
|
|
_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::slotReadPasswordJobDone);
|
|
job->start();
|
|
}
|
|
|
|
void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
|
|
QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob);
|
|
QKeychain::Error error = job->error();
|
|
|
|
// If we could not find the entry try the old entries
|
|
if (!_keychainMigration && error == QKeychain::EntryNotFound) {
|
|
_keychainMigration = true;
|
|
fetchFromKeychainHelper();
|
|
return;
|
|
}
|
|
|
|
if (_user.isEmpty()) {
|
|
qCWarning(lcWebFlowCredentials) << "Strange: User is empty!";
|
|
}
|
|
|
|
if (error == QKeychain::NoError) {
|
|
_password = job->textData();
|
|
_ready = true;
|
|
_credentialsValid = true;
|
|
} else {
|
|
_ready = false;
|
|
}
|
|
emit fetched();
|
|
|
|
// If keychain data was read from legacy location, wipe these entries and store new ones
|
|
if (_keychainMigration && _ready) {
|
|
_keychainMigration = false;
|
|
persist();
|
|
deleteKeychainEntries(true); // true: delete old entries
|
|
qCInfo(lcWebFlowCredentials) << "Migrated old keychain entries";
|
|
}
|
|
}
|
|
|
|
void WebFlowCredentials::deleteKeychainEntries(bool oldKeychainEntries) {
|
|
auto startDeleteJob = [this, oldKeychainEntries](QString user) {
|
|
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
|
addSettingsToJob(_account, job);
|
|
job->setInsecureFallback(false);
|
|
job->setKey(keychainKey(_account->url().toString(),
|
|
user,
|
|
oldKeychainEntries ? QString() : _account->id()));
|
|
job->start();
|
|
};
|
|
|
|
startDeleteJob(_user);
|
|
|
|
/* IMPORTANT - remove later - FIXME MS@2019-12-07 -->
|
|
* TODO: For "Log out" & "Remove account": Remove client CA certs and KEY!
|
|
*
|
|
* Disabled as long as selecting another cert is not supported by the UI.
|
|
*
|
|
* Being able to specify a new certificate is important anyway: expiry etc.
|
|
*
|
|
* We introduce this dirty hack here, to allow deleting them upon Remote Wipe.
|
|
*/
|
|
if(_account->isRemoteWipeRequested_HACK()) {
|
|
// <-- FIXME MS@2019-12-07
|
|
startDeleteJob(_user + clientKeyPEMC);
|
|
startDeleteJob(_user + clientCertificatePEMC);
|
|
|
|
for (auto i = 0; i < _clientSslCaCertificates.count(); i++) {
|
|
startDeleteJob(_user + clientCaCertificatePEMC + QString::number(i));
|
|
}
|
|
|
|
#if defined(Q_OS_WIN)
|
|
// also delete key sub-chunks (Windows workaround)
|
|
for (auto i = 1; i < _clientSslKeyChunkCount; i++) {
|
|
startDeleteJob(_user + clientKeyPEMC + QString(".") + QString::number(i));
|
|
}
|
|
#endif
|
|
// FIXME MS@2019-12-07 -->
|
|
}
|
|
// <-- FIXME MS@2019-12-07
|
|
}
|
|
|
|
}
|