Client certs: Store pkcs12 in config, password in keychain
authorChristian Kamm <mail@ckamm.de>
Tue, 19 Feb 2019 10:38:46 +0000 (11:38 +0100)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Tue, 15 Dec 2020 09:58:43 +0000 (10:58 +0100)
It still reads and writes the old format too, but all newly stored
client certs will be in the new form.

For #6776 because Windows limits credential data to 512 bytes in older
versions.

src/gui/addcertificatedialog.ui
src/gui/creds/httpcredentialsgui.h
src/gui/wizard/owncloudhttpcredspage.cpp
src/gui/wizard/owncloudoauthcredspage.cpp
src/gui/wizard/owncloudsetuppage.cpp
src/gui/wizard/owncloudwizard.h
src/libsync/account.cpp
src/libsync/creds/httpcredentials.cpp
src/libsync/creds/httpcredentials.h

index cf1684254d6bbdd44f08e42ed5919f7e0b503f3b..06789745dfae976bd5fc3a92ce51f72e8cf057ad 100644 (file)
@@ -9,8 +9,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>462</width>
-    <height>188</height>
+    <width>478</width>
+    <height>225</height>
    </rect>
   </property>
   <property name="windowTitle">
      </item>
     </layout>
    </item>
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>An encrypted pkcs12 bundle is strongly recommended as a copy will be stored in the configuration file.</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
    <item>
     <widget class="QLabel" name="labelErrorCertif">
      <property name="palette">
       <palette>
        <active>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="255">
+           <red>255</red>
+           <green>0</green>
+           <blue>0</blue>
+          </color>
+         </brush>
+        </colorrole>
         <colorrole role="Text">
          <brush brushstyle="SolidPattern">
           <color alpha="255">
         </colorrole>
        </active>
        <inactive>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="255">
+           <red>255</red>
+           <green>0</green>
+           <blue>0</blue>
+          </color>
+         </brush>
+        </colorrole>
         <colorrole role="Text">
          <brush brushstyle="SolidPattern">
           <color alpha="255">
         </colorrole>
        </inactive>
        <disabled>
+        <colorrole role="WindowText">
+         <brush brushstyle="SolidPattern">
+          <color alpha="255">
+           <red>119</red>
+           <green>120</green>
+           <blue>120</blue>
+          </color>
+         </brush>
+        </colorrole>
         <colorrole role="Text">
          <brush brushstyle="SolidPattern">
           <color alpha="255">
        </disabled>
       </palette>
      </property>
-     <property name="text">
-      <string/>
-     </property>
      <property name="wordWrap">
       <bool>true</bool>
      </property>
index 9f4e055a4721e845a9135ab3b8596f683c6c5615..fd4161dbb2a02e727c6351a9309e20accd9ad56b 100644 (file)
@@ -33,13 +33,14 @@ public:
         : HttpCredentials()
     {
     }
-    HttpCredentialsGui(const QString &user, const QString &password, const QSslCertificate &certificate, const QSslKey &key)
-        : HttpCredentials(user, password, certificate, key)
+    HttpCredentialsGui(const QString &user, const QString &password,
+            const QByteArray &clientCertBundle, const QByteArray &clientCertPassword)
+        : HttpCredentials(user, password, clientCertBundle, clientCertPassword)
     {
     }
     HttpCredentialsGui(const QString &user, const QString &password, const QString &refreshToken,
-        const QSslCertificate &certificate, const QSslKey &key)
-        : HttpCredentials(user, password, certificate, key)
+            const QByteArray &clientCertBundle, const QByteArray &clientCertPassword)
+        : HttpCredentials(user, password, clientCertBundle, clientCertPassword)
     {
         _refreshToken = refreshToken;
     }
index 02a82c45910ed168286466609c9e07a466526379..cab2a7427b50af8fdb4537a8d2fd265e4c6eeaaa 100644 (file)
@@ -191,7 +191,7 @@ void OwncloudHttpCredsPage::setErrorString(const QString &err)
 
 AbstractCredentials *OwncloudHttpCredsPage::getCredentials() const
 {
-    return new HttpCredentialsGui(_ui.leUsername->text(), _ui.lePassword->text(), _ocWizard->_clientSslCertificate, _ocWizard->_clientSslKey);
+    return new HttpCredentialsGui(_ui.leUsername->text(), _ui.lePassword->text(), _ocWizard->_clientCertBundle, _ocWizard->_clientCertPassword);
 }
 
 void OwncloudHttpCredsPage::slotStyleChanged()
index 79f36ba364af6cb88b23a4f0e915a9c7f5be96e6..267830dc1c80310c6bab0b3fc6072f94959bb667 100644 (file)
@@ -112,7 +112,7 @@ AbstractCredentials *OwncloudOAuthCredsPage::getCredentials() const
     auto *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
     Q_ASSERT(ocWizard);
     return new HttpCredentialsGui(_user, _token, _refreshToken,
-        ocWizard->_clientSslCertificate, ocWizard->_clientSslKey);
+        ocWizard->_clientCertBundle, ocWizard->_clientCertPassword);
 }
 
 bool OwncloudOAuthCredsPage::isComplete() const
index 3fac734d0b1cf557c677dbbe787a600f4940f77c..13538827aa64f3012d77851c950e1f75f6d3bf32 100644 (file)
@@ -24,6 +24,7 @@
 #include <QNetworkAccessManager>
 #include <QPropertyAnimation>
 #include <QGraphicsPixmapItem>
+#include <QBuffer>
 
 #include "QProgressIndicator.h"
 
@@ -365,14 +366,20 @@ void OwncloudSetupPage::slotCertificateAccepted()
 {
     QFile certFile(addCertDial->getCertificatePath());
     certFile.open(QFile::ReadOnly);
-    if (QSslCertificate::importPkcs12(
-            &certFile,
-            &_ocWizard->_clientSslKey,
-            &_ocWizard->_clientSslCertificate,
-            &_ocWizard->_clientSslCaCertificates,
-            addCertDial->getCertificatePasswd().toLocal8Bit())) {
-        // The SSL cert gets added to the QSslConfiguration in checkServer()
+    QByteArray certData = certFile.readAll();
+    QByteArray certPassword = addCertDial->getCertificatePasswd().toLocal8Bit();
+
+    QBuffer certDataBuffer(&certData);
+    certDataBuffer.open(QIODevice::ReadOnly);
+    if (QSslCertificate::importPkcs12(&certDataBuffer,
+            &_ocWizard->_clientSslKey, &_ocWizard->_clientSslCertificate,
+            &_ocWizard->_clientSslCaCertificates, certPassword)) {
+        _ocWizard->_clientCertBundle = certData;
+        _ocWizard->_clientCertPassword = certPassword;
+
         addCertDial->reinit(); // FIXME: Why not just have this only created on use?
+
+        // The extracted SSL key and cert gets added to the QSslConfiguration in checkServer()
         validatePage();
     } else {
         addCertDial->showErrorMessage(tr("Could not load certificate. Maybe wrong password?"));
index 5a61a741fb42fe545c8520ec58c3fd6f5b570405..3dd479d597565aa16c356ee84551ec52559c5494 100644 (file)
@@ -86,8 +86,10 @@ public:
 
     // FIXME: Can those be local variables?
     // Set from the OwncloudSetupPage, later used from OwncloudHttpCredsPage
-    QSslKey _clientSslKey;
-    QSslCertificate _clientSslCertificate;
+    QByteArray _clientCertBundle; // raw, potentially encrypted pkcs12 bundle provided by the user
+    QByteArray _clientCertPassword; // password for the pkcs12
+    QSslKey _clientSslKey; // key extracted from pkcs12
+    QSslCertificate _clientSslCertificate; // cert extracted from pkcs12
     QList<QSslCertificate> _clientSslCaCertificates;
 
 public slots:
index cde76ceb5911d678d2290b9b86e0faa77e39d2c1..b13fd329ddb5f149bd981bf7efae69fc4641f20a 100644 (file)
@@ -359,9 +359,9 @@ QVariant Account::credentialSetting(const QString &key) const
 {
     if (_credentials) {
         QString prefix = _credentials->authType();
-        QString value = _settingsMap.value(prefix + "_" + key).toString();
-        if (value.isEmpty()) {
-            value = _settingsMap.value(key).toString();
+        QVariant value = _settingsMap.value(prefix + "_" + key);
+        if (value.isNull()) {
+            value = _settingsMap.value(key);
         }
         return value;
     }
index c2c28bcb527d6b30fab0adf1a4e3e97e646cc933..21bb5559c0900a46e635c10aef717b87b89bdb08 100644 (file)
@@ -42,6 +42,8 @@ Q_LOGGING_CATEGORY(lcHttpCredentials, "nextcloud.sync.credentials.http", QtInfoM
 namespace {
     const char userC[] = "user";
     const char isOAuthC[] = "oauth";
+    const char clientCertBundleC[] = "clientCertPkcs12";
+    const char clientCertPasswordC[] = "_clientCertPassword";
     const char clientCertificatePEMC[] = "_clientCertificatePEM";
     const char clientKeyPEMC[] = "_clientKeyPEM";
     const char authenticationFailedC[] = "owncloud-authentication-failed";
@@ -114,14 +116,17 @@ static void addSettingsToJob(Account *account, QKeychain::Job *job)
 HttpCredentials::HttpCredentials() = default;
 
 // From wizard
-HttpCredentials::HttpCredentials(const QString &user, const QString &password, const QSslCertificate &certificate, const QSslKey &key)
+HttpCredentials::HttpCredentials(const QString &user, const QString &password, const QByteArray &clientCertBundle, const QByteArray &clientCertPassword)
     : _user(user)
     , _password(password)
     , _ready(true)
-    , _clientSslKey(key)
-    , _clientSslCertificate(certificate)
+    , _clientCertBundle(clientCertBundle)
+    , _clientCertPassword(clientCertPassword)
     , _retryOnKeyChainError(false)
 {
+    if (!unpackClientCertBundle()) {
+        ASSERT(false, "pkcs12 client cert bundle passed to HttpCredentials must be valid");
+    }
 }
 
 QString HttpCredentials::authType() const
@@ -192,7 +197,20 @@ void HttpCredentials::fetchFromKeychain()
 
 void HttpCredentials::fetchFromKeychainHelper()
 {
-    // Read client cert from keychain
+    _clientCertBundle = _account->credentialSetting(QLatin1String(clientCertBundleC)).toByteArray();
+    if (!_clientCertBundle.isEmpty()) {
+        // New case (>=2.6): We have a bundle in the settings and read the password from
+        // the keychain
+        ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
+        addSettingsToJob(_account, job);
+        job->setInsecureFallback(false);
+        job->setKey(keychainKey(_account->url().toString(), _user + clientCertPasswordC, _account->id()));
+        connect(job, &Job::finished, this, &HttpCredentials::slotReadClientCertPasswordJobDone);
+        job->start();
+        return;
+    }
+
+    // Old case (pre 2.6): Read client cert and then key from keychain
     const QString kck = keychainKey(
         _account->url().toString(),
         _user + clientCertificatePEMC,
@@ -221,7 +239,7 @@ void HttpCredentials::deleteOldKeychainEntries()
     startDeleteJob(_user + clientCertificatePEMC);
 }
 
-void HttpCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incoming)
+bool HttpCredentials::keychainUnavailableRetryLater(QKeychain::Job *incoming)
 {
 #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
     Q_ASSERT(!incoming->insecureFallback()); // If insecureFallback is set, the next test would be pointless
@@ -233,10 +251,38 @@ void HttpCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incoming)
         qCInfo(lcHttpCredentials) << "Backend unavailable (yet?) Retrying in a few seconds." << incoming->errorString();
         QTimer::singleShot(10000, this, &HttpCredentials::fetchFromKeychainHelper);
         _retryOnKeyChainError = false;
-        return;
+        return true;
     }
-    _retryOnKeyChainError = false;
 #endif
+    _retryOnKeyChainError = false;
+    return false;
+}
+
+void HttpCredentials::slotReadClientCertPasswordJobDone(QKeychain::Job *job)
+{
+    if (keychainUnavailableRetryLater(job))
+        return;
+
+    ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(job);
+    if (readJob->error() == NoError) {
+        _clientCertPassword = readJob->binaryData();
+    } else {
+        qCWarning(lcHttpCredentials) << "Could not retrieve client cert password from keychain" << job->errorString();
+    }
+
+    if (!unpackClientCertBundle()) {
+        qCWarning(lcHttpCredentials) << "Could not unpack client cert bundle";
+    }
+    _clientCertBundle.clear();
+    _clientCertPassword.clear();
+
+    slotReadPasswordFromKeychain();
+}
+
+void HttpCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incoming)
+{
+    if (keychainUnavailableRetryLater(incoming))
+        return;
 
     // Store PEM in memory
     auto *readJob = static_cast<ReadPasswordJob *>(incoming);
@@ -282,7 +328,11 @@ void HttpCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incoming)
         }
     }
 
-    // Now fetch the actual server password
+    slotReadPasswordFromKeychain();
+}
+
+void HttpCredentials::slotReadPasswordFromKeychain()
+{
     const QString kck = keychainKey(
         _account->url().toString(),
         _user,
@@ -468,10 +518,32 @@ void HttpCredentials::persist()
 
     _account->setCredentialSetting(QLatin1String(userC), _user);
     _account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
+    if (!_clientCertBundle.isEmpty()) {
+        // Note that the _clientCertBundle will often be cleared after usage,
+        // it's just written if it gets passed into the constructor.
+        _account->setCredentialSetting(QLatin1String(clientCertBundleC), _clientCertBundle);
+    }
     _account->wantsAccountSaved(_account);
 
-    // write cert if there is one
-    if (!_clientSslCertificate.isNull()) {
+    // write secrets to the keychain
+    if (!_clientCertBundle.isEmpty()) {
+        // Option 1: If we have a pkcs12 bundle, that'll be written to the config file
+        // and we'll just store the bundle password in the keychain. That's prefered
+        // since the keychain on older Windows platforms can only store a limited number
+        // of bytes per entry and key/cert may exceed that.
+        auto *job = new WritePasswordJob(Theme::instance()->appName());
+        addSettingsToJob(_account, job);
+        job->setInsecureFallback(false);
+        connect(job, &Job::finished, this, &HttpCredentials::slotWriteClientCertPasswordJobDone);
+        job->setKey(keychainKey(_account->url().toString(), _user + clientCertPasswordC, _account->id()));
+        job->setBinaryData(_clientCertPassword);
+        job->start();
+        _clientCertBundle.clear();
+        _clientCertPassword.clear();
+    } else if (_account->credentialSetting(QLatin1String(clientCertBundleC)).isNull() && !_clientSslCertificate.isNull()) {
+        // Option 2, pre 2.6 configs: We used to store the raw cert/key in the keychain and
+        // still do so if no bundle is available. We can't currently migrate to Option 1
+        // because we have no functions for creating an encrypted pkcs12 bundle.
         auto *job = new WritePasswordJob(Theme::instance()->appName());
         addSettingsToJob(_account, job);
         job->setInsecureFallback(false);
@@ -480,10 +552,21 @@ void HttpCredentials::persist()
         job->setBinaryData(_clientSslCertificate.toPem());
         job->start();
     } else {
-        slotWriteClientCertPEMJobDone(nullptr);
+        // Option 3: no client certificate at all (or doesn't need to be written)
+        slotWritePasswordToKeychain();
     }
 }
 
+void HttpCredentials::slotWriteClientCertPasswordJobDone(Job *finishedJob)
+{
+    if (finishedJob && finishedJob->error() != QKeychain::NoError) {
+        qCWarning(lcHttpCredentials) << "Could not write client cert password to credentials"
+                                     << finishedJob->error() << finishedJob->errorString();
+    }
+
+    slotWritePasswordToKeychain();
+}
+
 void HttpCredentials::slotWriteClientCertPEMJobDone(Job *finishedJob)
 {
     if (finishedJob && finishedJob->error() != QKeychain::NoError) {
@@ -512,6 +595,11 @@ void HttpCredentials::slotWriteClientKeyPEMJobDone(Job *finishedJob)
                                      << finishedJob->error() << finishedJob->errorString();
     }
 
+    slotWritePasswordToKeychain();
+}
+
+void HttpCredentials::slotWritePasswordToKeychain()
+{
     auto *job = new WritePasswordJob(Theme::instance()->appName());
     addSettingsToJob(_account, job);
     job->setInsecureFallback(false);
@@ -561,4 +649,16 @@ bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
     return true;
 }
 
+bool HttpCredentials::unpackClientCertBundle()
+{
+    if (_clientCertBundle.isEmpty())
+        return true;
+
+    QBuffer certBuffer(&_clientCertBundle);
+    certBuffer.open(QIODevice::ReadOnly);
+    QList<QSslCertificate> clientCaCertificates;
+    return QSslCertificate::importPkcs12(
+            &certBuffer, &_clientSslKey, &_clientSslCertificate, &clientCaCertificates, _clientCertPassword);
+}
+
 } // namespace OCC
index 2ac089f09845bfc7b4c109a801dba48d797d2601..04c7efc9add8cf6d77e2bb8b23d065c8438f38a4 100644 (file)
@@ -80,7 +80,8 @@ public:
     static constexpr QNetworkRequest::Attribute DontAddCredentialsAttribute = QNetworkRequest::User;
 
     HttpCredentials();
-    explicit HttpCredentials(const QString &user, const QString &password, const QSslCertificate &certificate = QSslCertificate(), const QSslKey &key = QSslKey());
+    explicit HttpCredentials(const QString &user, const QString &password,
+            const QByteArray &clientCertBundle = QByteArray(), const QByteArray &clientCertPassword = QByteArray());
 
     QString authType() const override;
     QNetworkAccessManager *createQNAM() const override;
@@ -112,12 +113,18 @@ public:
 private Q_SLOTS:
     void slotAuthentication(QNetworkReply *, QAuthenticator *);
 
+    void slotReadClientCertPasswordJobDone(QKeychain::Job *);
     void slotReadClientCertPEMJobDone(QKeychain::Job *);
     void slotReadClientKeyPEMJobDone(QKeychain::Job *);
+
+    void slotReadPasswordFromKeychain();
     void slotReadJobDone(QKeychain::Job *);
 
+    void slotWriteClientCertPasswordJobDone(QKeychain::Job *);
     void slotWriteClientCertPEMJobDone(QKeychain::Job *);
     void slotWriteClientKeyPEMJobDone(QKeychain::Job *);
+
+    void slotWritePasswordToKeychain();
     void slotWriteJobDone(QKeychain::Job *);
 
 protected:
@@ -133,6 +140,22 @@ protected:
     /// Wipes legacy keychain locations
     void deleteOldKeychainEntries();
 
+    /** Whether to bow out now because a retry will happen later
+     *
+     * Sometimes the keychain needs a while to become available.
+     * This function should be called on first keychain-read to check
+     * whether it errored because the keychain wasn't available yet.
+     * If that happens, this function will schedule another try and
+     * return true.
+     */
+    bool keychainUnavailableRetryLater(QKeychain::Job *);
+
+    /** Takes client cert pkcs12 and unwraps the key/cert.
+     *
+     * Returns false on failure.
+     */
+    bool unpackClientCertBundle();
+
     QString _user;
     QString _password; // user's password, or access_token for OAuth
     QString _refreshToken; // OAuth _refreshToken, set if OAuth is used.
@@ -141,6 +164,8 @@ protected:
     QString _fetchErrorString;
     bool _ready = false;
     bool _isRenewingOAuthToken = false;
+    QByteArray _clientCertBundle;
+    QByteArray _clientCertPassword;
     QSslKey _clientSslKey;
     QSslCertificate _clientSslCertificate;
     bool _keychainMigration = false;