Credentials: Use per-account keychain entries #5830
authorChristian Kamm <mail@ckamm.de>
Tue, 12 Sep 2017 15:15:22 +0000 (17:15 +0200)
committerRoeland Jago Douma <roeland@famdouma.nl>
Thu, 5 Oct 2017 20:01:32 +0000 (22:01 +0200)
This requires a lot of migration code: the old entries need to be read,
saved to the new locations and then deleted.

src/gui/creds/shibbolethcredentials.cpp
src/gui/creds/shibbolethcredentials.h
src/libsync/creds/abstractcredentials.cpp
src/libsync/creds/abstractcredentials.h
src/libsync/creds/httpcredentials.cpp
src/libsync/creds/httpcredentials.h

index 44c0e217a9315daec460fc5bf088326b2dcd20b9..6f5641b35908cdb6b85e0a8910debac9dc380b52 100644 (file)
@@ -54,6 +54,7 @@ ShibbolethCredentials::ShibbolethCredentials()
     , _ready(false)
     , _stillValid(false)
     , _browser(0)
+    , _keychainMigration(false)
 {
 }
 
@@ -62,6 +63,7 @@ ShibbolethCredentials::ShibbolethCredentials(const QNetworkCookie &cookie)
     , _stillValid(true)
     , _browser(0)
     , _shibCookie(cookie)
+    , _keychainMigration(false)
 {
 }
 
@@ -131,15 +133,22 @@ void ShibbolethCredentials::fetchFromKeychain()
         Q_EMIT fetched();
     } else {
         _url = _account->url();
-        ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
-        job->setSettings(ConfigFile::settingsWithGroup(Theme::instance()->appName(), job).release());
-        job->setInsecureFallback(false);
-        job->setKey(keychainKey(_account->url().toString(), user()));
-        connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadJobDone(QKeychain::Job *)));
-        job->start();
+        _keychainMigration = false;
+        fetchFromKeychainHelper();
     }
 }
 
+void ShibbolethCredentials::fetchFromKeychainHelper()
+{
+    ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
+    job->setSettings(ConfigFile::settingsWithGroup(Theme::instance()->appName(), job).release());
+    job->setInsecureFallback(false);
+    job->setKey(keychainKey(_url.toString(), user(),
+        _keychainMigration ? QString() : _account->id()));
+    connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadJobDone(QKeychain::Job *)));
+    job->start();
+}
+
 void ShibbolethCredentials::askFromUser()
 {
     showLoginWindow();
@@ -242,6 +251,16 @@ void ShibbolethCredentials::slotBrowserRejected()
 
 void ShibbolethCredentials::slotReadJobDone(QKeychain::Job *job)
 {
+    // If we can't find the credentials at the keys that include the account id,
+    // try to read them from the legacy locations that don't have a account id.
+    if (!_keychainMigration && job->error() == QKeychain::EntryNotFound) {
+        qCWarning(lcShibboleth)
+            << "Could not find keychain entry, attempting to read from legacy location";
+        _keychainMigration = true;
+        fetchFromKeychainHelper();
+        return;
+    }
+
     if (job->error() == QKeychain::NoError) {
         ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(job);
         delete readJob->settings();
@@ -260,6 +279,19 @@ void ShibbolethCredentials::slotReadJobDone(QKeychain::Job *job)
         _ready = false;
         Q_EMIT fetched();
     }
+
+
+    // If keychain data was read from legacy location, wipe these entries and store new ones
+    if (_keychainMigration && _ready) {
+        persist();
+
+        DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
+        job->setSettings(ConfigFile::settingsWithGroup(Theme::instance()->appName(), job).release());
+        job->setKey(keychainKey(_account->url().toString(), user(), QString()));
+        job->start();
+
+        qCWarning(lcShibboleth) << "Migrated old keychain entries";
+    }
 }
 
 void ShibbolethCredentials::showLoginWindow()
@@ -313,7 +345,7 @@ void ShibbolethCredentials::storeShibCookie(const QNetworkCookie &cookie)
     job->setSettings(ConfigFile::settingsWithGroup(Theme::instance()->appName(), job).release());
     // we don't really care if it works...
     //connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotWriteJobDone(QKeychain::Job*)));
-    job->setKey(keychainKey(_account->url().toString(), user()));
+    job->setKey(keychainKey(_account->url().toString(), user(), _account->id()));
     job->setTextData(QString::fromUtf8(cookie.toRawForm()));
     job->start();
 }
@@ -322,7 +354,7 @@ void ShibbolethCredentials::removeShibCookie()
 {
     DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
     job->setSettings(ConfigFile::settingsWithGroup(Theme::instance()->appName(), job).release());
-    job->setKey(keychainKey(_account->url().toString(), user()));
+    job->setKey(keychainKey(_account->url().toString(), user(), _account->id()));
     job->start();
 }
 
@@ -336,5 +368,4 @@ void ShibbolethCredentials::addToCookieJar(const QNetworkCookie &cookie)
     jar->blockSignals(false);
 }
 
-
 } // namespace OCC
index a4909ed7412df24a5d57f51edb5718cf94471ad8..3ff519a29b26bab429caf0c1bb641c1161107a92 100644 (file)
@@ -84,6 +84,10 @@ private:
     void storeShibCookie(const QNetworkCookie &cookie);
     void removeShibCookie();
     void addToCookieJar(const QNetworkCookie &cookie);
+
+    /// Reads data from keychain, progressing to slotReadJobDone
+    void fetchFromKeychainHelper();
+
     QUrl _url;
     QByteArray prepareCookieData() const;
 
@@ -92,6 +96,7 @@ private:
     QPointer<ShibbolethWebView> _browser;
     QNetworkCookie _shibCookie;
     QString _user;
+    bool _keychainMigration;
 };
 
 } // namespace OCC
index 2aca3a0f8b521fe2bd1a27327ed3cdf9d631f608..2dadc07938ba5090ccff51405e6b59cfe653d9b6 100644 (file)
@@ -34,7 +34,7 @@ void AbstractCredentials::setAccount(Account *account)
     _account = account;
 }
 
-QString AbstractCredentials::keychainKey(const QString &url, const QString &user)
+QString AbstractCredentials::keychainKey(const QString &url, const QString &user, const QString &accountId)
 {
     QString u(url);
     if (u.isEmpty()) {
@@ -51,6 +51,9 @@ QString AbstractCredentials::keychainKey(const QString &url, const QString &user
     }
 
     QString key = user + QLatin1Char(':') + u;
+    if (!accountId.isEmpty()) {
+        key += QLatin1Char(':') + accountId;
+    }
     return key;
 }
 } // namespace OCC
index 9958b56666ac8b5f34ba549d629b722198a35019..41807c4192278d4f6fd336fb089ca82fcf3f1b0e 100644 (file)
@@ -85,7 +85,7 @@ public:
      */
     virtual void forgetSensitiveData() = 0;
 
-    static QString keychainKey(const QString &url, const QString &user);
+    static QString keychainKey(const QString &url, const QString &user, const QString &accountId);
 
 Q_SIGNALS:
     /** Emitted when fetchFromKeychain() is done.
index d88791d7217cd6487f6673ee2ea90bfe5cdb3026..262ad9309c0821efc9794a0f59e205ca0a05f410 100644 (file)
@@ -104,6 +104,7 @@ static void addSettingsToJob(Account *account, QKeychain::Job *job)
 
 HttpCredentials::HttpCredentials()
     : _ready(false)
+    , _keychainMigration(false)
 {
 }
 
@@ -114,6 +115,7 @@ HttpCredentials::HttpCredentials(const QString &user, const QString &password, c
     , _ready(true)
     , _clientSslKey(key)
     , _clientSslCertificate(certificate)
+    , _keychainMigration(false)
 {
 }
 
@@ -175,21 +177,43 @@ void HttpCredentials::fetchFromKeychain()
         return;
     }
 
-    const QString kck = keychainKey(_account->url().toString(), _user);
-
     if (_ready) {
         Q_EMIT fetched();
     } else {
-        // Read client cert from keychain
-        const QString kck = keychainKey(_account->url().toString(), _user + clientCertificatePEMC);
-        ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
-        addSettingsToJob(_account, job);
-        job->setInsecureFallback(false);
-        job->setKey(kck);
+        _keychainMigration = false;
+        fetchFromKeychainHelper();
+    }
+}
 
-        connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadClientCertPEMJobDone(QKeychain::Job *)));
+void HttpCredentials::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, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadClientCertPEMJobDone(QKeychain::Job *)));
+    job->start();
+}
+
+void HttpCredentials::deleteOldKeychainEntries()
+{
+    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);
 }
 
 void HttpCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incoming)
@@ -204,12 +228,15 @@ void HttpCredentials::slotReadClientCertPEMJobDone(QKeychain::Job *incoming)
     }
 
     // Load key too
-    const QString kck = keychainKey(_account->url().toString(), _user + clientKeyPEMC);
+    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, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadClientKeyPEMJobDone(QKeychain::Job *)));
     job->start();
 }
@@ -236,12 +263,15 @@ void HttpCredentials::slotReadClientKeyPEMJobDone(QKeychain::Job *incoming)
     }
 
     // Now fetch the actual server password
-    const QString kck = keychainKey(_account->url().toString(), _user);
+    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, SIGNAL(finished(QKeychain::Job *)), SLOT(slotReadJobDone(QKeychain::Job *)));
     job->start();
 }
@@ -258,6 +288,17 @@ bool HttpCredentials::stillValid(QNetworkReply *reply)
 void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
 {
     QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob);
+    QKeychain::Error error = job->error();
+
+    // If we can't find the credentials at the keys that include the account id,
+    // try to read them from the legacy locations that don't have a account id.
+    if (!_keychainMigration && error == QKeychain::EntryNotFound) {
+        qCWarning(lcHttpCredentials)
+            << "Could not find keychain entries, attempting to read from legacy locations";
+        _keychainMigration = true;
+        fetchFromKeychainHelper();
+        return;
+    }
 
     bool isOauth = _account->credentialSetting(QLatin1String(isOAuthC)).toBool();
     if (isOauth) {
@@ -270,8 +311,6 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
         qCWarning(lcHttpCredentials) << "Strange: User is empty!";
     }
 
-    QKeychain::Error error = job->error();
-
     if (!_refreshToken.isEmpty() && error == NoError) {
         refreshAccessToken();
     } else if (!_password.isEmpty() && error == NoError) {
@@ -290,6 +329,13 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
         _ready = false;
         emit fetched();
     }
+
+    // If keychain data was read from legacy location, wipe these entries and store new ones
+    if (_keychainMigration && _ready) {
+        persist();
+        deleteOldKeychainEntries();
+        qCWarning(lcHttpCredentials) << "Migrated old keychain entries";
+    }
 }
 
 bool HttpCredentials::refreshAccessToken()
@@ -345,7 +391,7 @@ void HttpCredentials::invalidateToken()
     // User must be fetched from config file to generate a valid key
     fetchUser();
 
-    const QString kck = keychainKey(_account->url().toString(), _user);
+    const QString kck = keychainKey(_account->url().toString(), _user, _account->id());
     if (kck.isEmpty()) {
         qCWarning(lcHttpCredentials) << "InvalidateToken: User is empty, bailing out!";
         return;
@@ -407,7 +453,7 @@ void HttpCredentials::persist()
     addSettingsToJob(_account, job);
     job->setInsecureFallback(false);
     connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotWriteClientCertPEMJobDone(QKeychain::Job *)));
-    job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC));
+    job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
     job->setBinaryData(_clientSslCertificate.toPem());
     job->start();
 }
@@ -420,7 +466,7 @@ void HttpCredentials::slotWriteClientCertPEMJobDone(Job *incomingJob)
     addSettingsToJob(_account, job);
     job->setInsecureFallback(false);
     connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotWriteClientKeyPEMJobDone(QKeychain::Job *)));
-    job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC));
+    job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC, _account->id()));
     job->setBinaryData(_clientSslKey.toPem());
     job->start();
 }
@@ -432,7 +478,7 @@ void HttpCredentials::slotWriteClientKeyPEMJobDone(Job *incomingJob)
     addSettingsToJob(_account, job);
     job->setInsecureFallback(false);
     connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotWriteJobDone(QKeychain::Job *)));
-    job->setKey(keychainKey(_account->url().toString(), _user));
+    job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
     job->setTextData(isUsingOAuth() ? _refreshToken : _password);
     job->start();
 }
index 0dc7ad7328a5801b787b4af36447add309994597..9814f96306a2a3f295e3793d282227254fe1e6cf 100644 (file)
@@ -119,6 +119,18 @@ private Q_SLOTS:
     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 _user;
     QString _password; // user's password, or access_token for OAuth
     QString _refreshToken; // OAuth _refreshToken, set if OAuth is used.
@@ -128,6 +140,7 @@ protected:
     bool _ready;
     QSslKey _clientSslKey;
     QSslCertificate _clientSslCertificate;
+    bool _keychainMigration;
 };