mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-24 14:05:58 +03:00
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:
parent
18404a128b
commit
dbde585049
2 changed files with 194 additions and 10 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue