Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for qtkeychain to store access tokens #484

Merged
merged 6 commits into from Feb 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .travis.yml
Expand Up @@ -28,6 +28,7 @@ matrix:
- qt57quickcontrols
- qt57quickcontrols2
- qt57tools
- qt5keychain-dev
- os: linux
dist: trusty
compiler: clang
Expand Down Expand Up @@ -62,12 +63,14 @@ matrix:
- qt512quickcontrols2
- qt512tools
- qt512translations
- qt5keychain-dev
- os: osx
env: [ 'PATH=/usr/local/opt/qt/bin:$PATH"' ]
addons:
homebrew:
packages:
- qt5
- qtkeychain

before_script:
- eval "${ENV_EVAL}"
Expand Down
5 changes: 5 additions & 0 deletions BUILDING.md
Expand Up @@ -51,12 +51,17 @@ hacking on Quaternion _and_ libQMatrixClient at the same time.
- any build system that works with CMake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work.
- libQMatrixClient development files (from your package management system), or
prebuilt libQMatrixClient (see "Getting the source code" above).
- optionaly [QtKeychain](https://github.com/frankosterfeld/qtkeychain) to store access tokens in libsecret keyring or similar providers.

#### Linux
Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to take a look at `CMakeLists.txt` to figure out which specific libraries Quaternion uses (or blindly run cmake and look at error messages). Note also that you'll need several Qt Quick plugins for Quaternion to work (without them, it will compile and run but won't show the messages timeline). In case of Xenial Xerus following line should get you everything necessary to build and run Quaternion:
```bash
sudo apt-get install git cmake qtdeclarative5-dev qtdeclarative5-qtquick2-plugin qtdeclarative5-controls-plugin qml-module-qtquick-controls qml-module-qtquick-controls2 qtmultimedia5-dev
```
To enable libsecret keyring support, install QtKeychain by
```bash
sudo apt-get install qt5keychain-dev
```
ddanilov marked this conversation as resolved.
Show resolved Hide resolved
On Fedora 26, the following command should be enough for building and running:
```bash
dnf install git cmake qt5-qtdeclarative-devel qt5-qtquickcontrols qt5-qtquickcontrols2 qt5-qtmultimedia-devel
Expand Down
16 changes: 15 additions & 1 deletion CMakeLists.txt
Expand Up @@ -42,7 +42,7 @@ foreach (FLAG "" all pedantic extra no-unused-parameter)
endforeach ()

# Find the libraries
find_package(Qt5 5.7 REQUIRED Widgets Network Quick Qml Gui LinguistTools Multimedia)
find_package(Qt5 5.7 REQUIRED Widgets Network Quick Qml Gui LinguistTools Multimedia DBus)
ddanilov marked this conversation as resolved.
Show resolved Hide resolved
# Qt5_Prefix is only used to show Qt path in message()
# Qt5_BinDir is where all the binary tools for Qt are
if (QT_QMAKE_EXECUTABLE)
Expand Down Expand Up @@ -85,6 +85,11 @@ else ()
endif ()
endif ()

find_package(Qt5Keychain QUIET)
if (Qt5Keychain_FOUND)
set(USE_KEYCHAIN ON)
endif()

message( STATUS )
message( STATUS "=============================================================================" )
message( STATUS " Quaternion Build Information" )
Expand Down Expand Up @@ -121,6 +126,9 @@ endif ()
if (USE_QQUICKWIDGET)
message( STATUS "Using QQuickWidget to render QML")
endif(USE_QQUICKWIDGET)
if (USE_KEYCHAIN)
message( STATUS "Using Qt5Keychain ${Qt5Keychain_VERSION} at ${Qt5Keychain_DIR}")
endif ()
message( STATUS "=============================================================================" )
message( STATUS )

Expand Down Expand Up @@ -200,6 +208,12 @@ if (USE_QQUICKWIDGET)
target_link_libraries(${APP_NAME} Qt5::QuickWidgets)
endif()

if(USE_KEYCHAIN)
target_compile_definitions(${APP_NAME} PRIVATE USE_KEYCHAIN)
target_link_libraries(${APP_NAME} ${QTKEYCHAIN_LIBRARIES})
include_directories(${QTKEYCHAIN_INCLUDE_DIR})
endif()

# macOS specific config for bundling
set_property(TARGET ${APP_NAME} PROPERTY
MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in")
Expand Down
126 changes: 126 additions & 0 deletions client/mainwindow.cpp
Expand Up @@ -34,6 +34,10 @@
#include <logging.h>
#include <user.h>

#ifdef USE_KEYCHAIN
#include <qt5keychain/keychain.h>
#endif

#include <QtCore/QTimer>
#include <QtCore/QDebug>
#include <QtCore/QStandardPaths>
Expand Down Expand Up @@ -404,6 +408,15 @@ inline QString accessTokenFileName(const AccountSettings& account)
}

QByteArray MainWindow::loadAccessToken(const AccountSettings& account)
{
#ifdef USE_KEYCHAIN
return loadAccessTokenFromKeyChain(account);
#else
return loadAccessTokenFromFile(account);
#endif
}

QByteArray MainWindow::loadAccessTokenFromFile(const AccountSettings& account)
{
QFile accountTokenFile { accessTokenFileName(account) };
if (accountTokenFile.open(QFile::ReadOnly))
Expand All @@ -421,8 +434,70 @@ QByteArray MainWindow::loadAccessToken(const AccountSettings& account)
return {};
}

#ifdef USE_KEYCHAIN
QByteArray MainWindow::loadAccessTokenFromKeyChain(const AccountSettings& account)
{
// check for existing token file
auto accessToken = loadAccessTokenFromFile(account);
if(!accessToken.isEmpty())
{
if(QMessageBox::warning(this,
tr("Access token file found"),
tr("Do you want to migrate the access token for %1 "
"from the file to keychain?").arg(account.userId()),
QMessageBox::Yes|QMessageBox::No) == QMessageBox::Yes)
{
qDebug() << "Migrating the access token from file to keychain for " << account.userId();
bool removed = false;
bool saved = saveAccessTokenToKeyChain(account, accessToken);
if(saved)
{
QFile accountTokenFile{accessTokenFileName(account)};
removed = accountTokenFile.remove();
}
if(!(saved && removed))
{
qDebug() << "Migrating the access token from file to keychain failed";
QMessageBox::warning(this,
tr("Couldn't migrate access token"),
tr("Quaternion couldn't migrate access token %1 "
"from the file to keychain.").arg(account.userId()),
QMessageBox::Close);
}
}
}

qDebug() << "Read access token from keychain for " << account.userId();
QKeychain::ReadPasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
QEventLoop loop;
QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();

if (job.error())
{
qWarning() << "Could not read access token from keychain: " << qPrintable(job.errorString());
return {};
}

return job.binaryData();
}
#endif

bool MainWindow::saveAccessToken(const AccountSettings& account,
const QByteArray& accessToken)
{
#ifdef USE_KEYCHAIN
return saveAccessTokenToKeyChain(account, accessToken);
#else
return saveAccessTokenToFile(account, accessToken);
#endif
}

bool MainWindow::saveAccessTokenToFile(const AccountSettings& account,
const QByteArray& accessToken)
{
// (Re-)Make a dedicated file for access_token.
QFile accountTokenFile { accessTokenFileName(account) };
Expand Down Expand Up @@ -465,6 +540,39 @@ bool MainWindow::saveAccessToken(const AccountSettings& account,
return false;
}

#ifdef USE_KEYCHAIN
bool MainWindow::saveAccessTokenToKeyChain(const AccountSettings& account,
const QByteArray& accessToken)
{
qDebug() << "Save access token to keychain for " << account.userId();
QKeychain::WritePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(account.userId());
job.setBinaryData(accessToken);
QEventLoop loop;
QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
KitsuneRal marked this conversation as resolved.
Show resolved Hide resolved

if (job.error())
{
qWarning() << "Could not save access token to keychain: " << qPrintable(job.errorString());
const auto button = QMessageBox::warning(this,
tr("Couldn't save access token"),
tr("Quaternion couldn't save the access token to keychain."
" Do you want to save the access token to file %1?").arg(accessTokenFileName(account)),
QMessageBox::Yes|QMessageBox::No);
if (button == QMessageBox::Yes) {
return saveAccessTokenToFile(account, accessToken);
} else {
return false;
}
}

return true;
}
#endif

void MainWindow::enableDebug()
{
chatRoomWidget->enableDebug();
Expand Down Expand Up @@ -788,6 +896,24 @@ void MainWindow::logout(Connection* c)

QFile(accessTokenFileName(AccountSettings(c->userId()))).remove();

#ifdef USE_KEYCHAIN
QKeychain::DeletePasswordJob job(qAppName());
job.setAutoDelete(false);
job.setKey(c->userId());
QEventLoop loop;
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
KitsuneRal marked this conversation as resolved.
Show resolved Hide resolved
if (job.error())
{
qWarning() << "Could not delete access token from keychain: " << qPrintable(job.errorString());
QMessageBox::warning(this,
tr("Couldn't delete access token"),
tr("Quaternion couldn't delete the access token from keychain."),
QMessageBox::Close);
}
#endif

c->logout();
}

Expand Down
6 changes: 6 additions & 0 deletions client/mainwindow.h
Expand Up @@ -116,8 +116,14 @@ class MainWindow: public QMainWindow
void loadSettings();
void saveSettings() const;
QByteArray loadAccessToken(const QMatrixClient::AccountSettings& account);
QByteArray loadAccessTokenFromFile(const QMatrixClient::AccountSettings& account);
QByteArray loadAccessTokenFromKeyChain(const QMatrixClient::AccountSettings &account);
bool saveAccessToken(const QMatrixClient::AccountSettings& account,
const QByteArray& accessToken);
bool saveAccessTokenToFile(const QMatrixClient::AccountSettings& account,
const QByteArray& accessToken);
bool saveAccessTokenToKeyChain(const QMatrixClient::AccountSettings& account,
const QByteArray& accessToken);
Connection* chooseConnection();
void showMillisToRecon(Connection* c);
};