Adds SSL client cert storage to webflow + Login Flow v2

The previous commit 50cd6af394 - Build a webflowcredentials
changed:

src/gui/wizard/flow2authcredspage.cpp in line 135 to use WebFlowCredentials
instead of HttpCredentials.
But the WebFlowCredentials class didn't include code to store and load SSL client
certificates and keys from the keychain.

This commit migrates the useful stuff from the old HttpCredentials class
into WebFlowCredentials.

Successfully tested on Windows. Please test on other systems and verify it's safe! :)

Signed-off-by: Michael Schuster <michael@schuster.ms>
This commit is contained in:
Michael Schuster 2019-08-27 03:32:21 +02:00
parent 18404a128b
commit dbde585049
No known key found for this signature in database
GPG key ID: 00819E3BF4177B28
2 changed files with 194 additions and 10 deletions

View file

@ -14,6 +14,7 @@
#include "accessmanager.h"
#include "account.h"
#include "configfile.h"
#include "theme.h"
#include "wizard/webview.h"
#include "webflowcredentialsdialog.h"
@ -24,6 +25,12 @@ namespace OCC {
Q_LOGGING_CATEGORY(lcWebFlowCredentials, "sync.credentials.webflow", QtInfoMsg)
namespace {
const char userC[] = "user";
const char clientCertificatePEMC[] = "_clientCertificatePEM";
const char clientKeyPEMC[] = "_clientKeyPEM";
} // ns
class WebFlowCredentialsAccessManager : public AccessManager
{
public:
@ -37,13 +44,21 @@ protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
{
QNetworkRequest req(request);
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
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);
req.setSslConfiguration(sslConfiguration);
}
return AccessManager::createRequest(op, req, outgoingData);
}
@ -53,10 +68,19 @@ private:
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)
{
}
@ -69,6 +93,7 @@ WebFlowCredentials::WebFlowCredentials(const QString &user, const QString &passw
, _ready(true)
, _credentialsValid(true)
, _keychainMigration(false)
, _retryOnKeyChainError(false)
{
}
@ -185,17 +210,65 @@ void WebFlowCredentials::persist() {
return;
}
_account->setCredentialSetting("user", _user);
_account->setCredentialSetting(userC, _user);
_account->wantsAccountSaved(_account);
//TODO: Add ssl cert and key storing
// 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()) {
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
connect(job, &Job::finished, this, &WebFlowCredentials::slotWriteClientKeyPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC, _account->id()));
job->setBinaryData(_clientSslKey.toPem());
job->start();
} else {
// no key, just write credentials
slotWriteClientKeyPEMJobDone();
}
}
void WebFlowCredentials::slotWriteClientKeyPEMJobDone()
{
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();
@ -236,7 +309,7 @@ void WebFlowCredentials::setAccount(Account *account) {
}
QString WebFlowCredentials::fetchUser() {
_user = _account->credentialSetting("user").toString();
_user = _account->credentialSetting(userC).toString();
return _user;
}
@ -267,12 +340,89 @@ void WebFlowCredentials::slotFinished(QNetworkReply *reply) {
}
void WebFlowCredentials::fetchFromKeychainHelper() {
// Read client cert from keychain
const QString kck = keychainKey(
_account->url().toString(),
_user + clientCertificatePEMC,
_keychainMigration ? QString() : _account->id());
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(false);
job->setKey(kck);
connect(job, &Job::finished, this, &WebFlowCredentials::slotReadClientCertPEMJobDone);
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
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)
{
// Store key in memory
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incomingJob);
if (readJob->error() == NoError && readJob->binaryData().length() > 0) {
QByteArray clientKeyPEM = readJob->binaryData();
// FIXME Unfortunately Qt has a bug and we can't just use QSsl::Opaque to let it
// load whatever we have. So we try until it works.
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Rsa);
if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Dsa);
}
if (_clientSslKey.isNull()) {
_clientSslKey = QSslKey(clientKeyPEM, QSsl::Ec);
}
if (_clientSslKey.isNull()) {
qCWarning(lcWebFlowCredentials) << "Could not load SSL key into Qt!";
}
}
// 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);
@ -290,6 +440,10 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
return;
}
if (_user.isEmpty()) {
qCWarning(lcWebFlowCredentials) << "Strange: User is empty!";
}
if (error == QKeychain::NoError) {
_password = job->textData();
_ready = true;
@ -309,10 +463,17 @@ void WebFlowCredentials::slotReadPasswordJobDone(Job *incomingJob) {
}
void WebFlowCredentials::deleteOldKeychainEntries() {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false);
job->setKey(keychainKey(_account->url().toString(), _user, QString()));
job->start();
auto startDeleteJob = [this](QString user) {
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(true);
job->setKey(keychainKey(_account->url().toString(), user, QString()));
job->start();
};
startDeleteJob(_user);
startDeleteJob(_user + clientKeyPEMC);
startDeleteJob(_user + clientCertificatePEMC);
}
}

View file

@ -3,6 +3,7 @@
#include <QSslCertificate>
#include <QSslKey>
#include <QNetworkRequest>
#include "creds/abstractcredentials.h"
@ -22,7 +23,12 @@ class WebFlowCredentialsDialog;
class WebFlowCredentials : public AbstractCredentials
{
Q_OBJECT
friend class WebFlowCredentialsAccessManager;
public:
/// Don't add credentials if this is set on a QNetworkRequest
static constexpr QNetworkRequest::Attribute DontAddCredentialsAttribute = QNetworkRequest::User;
explicit WebFlowCredentials();
WebFlowCredentials(const QString &user, const QString &password, const QSslCertificate &certificate = QSslCertificate(), const QSslKey &key = QSslKey());
@ -48,11 +54,27 @@ private slots:
void slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator);
void slotFinished(QNetworkReply *reply);
void slotReadPasswordJobDone(QKeychain::Job *incomingJob);
void slotAskFromUserCredentialsProvided(const QString &user, const QString &pass, const QString &host);
private:
void slotReadClientCertPEMJobDone(QKeychain::Job *incomingJob);
void slotReadClientKeyPEMJobDone(QKeychain::Job *incomingJob);
void slotReadPasswordJobDone(QKeychain::Job *incomingJob);
void slotWriteClientCertPEMJobDone();
void slotWriteClientKeyPEMJobDone();
void slotWriteJobDone(QKeychain::Job *);
protected:
/** Reads data from keychain locations
*
* Goes through
* slotReadClientCertPEMJobDone to
* slotReadClientCertPEMJobDone to
* slotReadJobDone
*/
void fetchFromKeychainHelper();
/// Wipes legacy keychain locations
void deleteOldKeychainEntries();
QString fetchUser();
@ -65,6 +87,7 @@ private:
bool _ready;
bool _credentialsValid;
bool _keychainMigration;
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
WebFlowCredentialsDialog *_askDialog;
};