Skip to content

Commit

Permalink
Usage of QtKeychain (#3704)
Browse files Browse the repository at this point in the history
Co-authored-by: Tomas Mizera <[email protected]>
  • Loading branch information
VitorVieiraZ and tomasMizera authored Feb 13, 2025
1 parent 7349837 commit d827818
Show file tree
Hide file tree
Showing 13 changed files with 426 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ jobs:
-DQT_ANDROID_SIGN_APK=Yes \
-DQT_ANDROID_SIGN_AAB=Yes \
-DUSE_MM_SERVER_API_KEY=Yes \
-DUSE_KEYCHAIN=No \
-DCMAKE_TOOLCHAIN_FILE=$QT_BASE/android_arm64_v8a/lib/cmake/Qt6/qt.toolchain.cmake \
-GNinja \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ jobs:
-DQT_HOST_PATH=${{ github.workspace }}/Qt/${{ env.QT_VERSION }}/macos \
-DIOS=TRUE \
-DUSE_MM_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DCMAKE_INSTALL_PREFIX:PATH=../install-Input \
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/arm64-ios \
-G "Xcode" \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=${{ github.workspace }}/Qt/${{ env.QT_VERSION }}/gcc_64 \
-DUSE_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/x64-linux \
-DQGIS_QUICK_DATA_PATH=${{ github.workspace }}/input/app/android/assets/qgis-data \
-DUSE_MM_SERVER_API_KEY=TRUE \
Expand Down Expand Up @@ -191,6 +192,7 @@ jobs:
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/x64-linux \
-DQGIS_QUICK_DATA_PATH=${{ github.workspace }}/input/app/android/assets/qgis-data \
-DUSE_MM_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-GNinja \
-S ../input
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=${{ github.workspace }}/Qt/${{ env.QT_VERSION }}/macos \
-DUSE_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/x64-osx \
-DQGIS_QUICK_DATA_PATH=${{ github.workspace }}/input/app/android/assets/qgis-data \
-DCOVERAGE=TRUE \
Expand Down Expand Up @@ -173,6 +174,7 @@ jobs:
-DCMAKE_INSTALL_PREFIX:PATH=../install-Input \
-DUSE_SERVER_API_KEY=TRUE \
-DUSE_MM_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/x64-osx \
-DQGIS_QUICK_DATA_PATH=${{ github.workspace }}/input/app/android/assets/qgis-data \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/macos_arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ jobs:
-DCMAKE_INSTALL_PREFIX:PATH=../install-Input \
-DUSE_SERVER_API_KEY=TRUE \
-DUSE_MM_SERVER_API_KEY=TRUE \
-DUSE_KEYCHAIN=No \
-DINPUT_SDK_PATH=${{ github.workspace }}/input-sdk/arm64-osx \
-DQGIS_QUICK_DATA_PATH=${{ github.workspace }}/input/app/android/assets/qgis-data \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jobs:
-DCMAKE_INSTALL_PREFIX:PATH=${{ steps.vars.outputs.WORKSPACE_DIR }}/install-Input ^
-DINPUT_SDK_PATH:PATH=${{ steps.vars.outputs.WORKSPACE_DIR }}/input-sdk/x64-windows ^
-DUSE_MM_SERVER_API_KEY=TRUE ^
-DUSE_KEYCHAIN=No ^
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache ^
-G "NMake Makefiles" ^
-S ${{ steps.vars.outputs.WORKSPACE_DIR }}/input ^
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ set(HAVE_BLUETOOTH
CACHE BOOL "Building with bluetooth position provider"
)

set(USE_KEYCHAIN
FALSE
CACHE
BOOL
"Wheter to use keychains/wallets to store credentials. If false, we use QSettings"
)

set(QT6_VERSION
${QT_VERSION_DEFAULT}
CACHE STRING "QT6 version to use"
Expand Down
9 changes: 9 additions & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ set(MM_CORE_HDRS
project.h
geodiffutils.h
projectchecksumcache.h
credentialstore.h
)

if (USE_MM_SERVER_API_KEY)
Expand All @@ -50,6 +51,14 @@ else ()
)
endif ()

if (USE_KEYCHAIN)
set(MM_CORE_SRCS ${MM_CORE_SRCS} credentialstorekeychain.cpp)
message(STATUS "Using QtKeychain to store credentials.")
else ()
set(MM_CORE_SRCS ${MM_CORE_SRCS} credentialstoreplaintext.cpp)
message(STATUS "Using QSettings to store credentials.")
endif ()

add_library(mm_core OBJECT ${MM_CORE_SRCS} ${MM_CORE_HDRS})
target_include_directories(mm_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(mm_core PRIVATE Qt6::Core Qt6::Network Geodiff::Geodiff)
Expand Down
75 changes: 75 additions & 0 deletions core/credentialstore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#ifndef CREDENTIALSTORE_H
#define CREDENTIALSTORE_H

#include <QObject>
#include <QString>
#include <QDateTime>

#include <qt6keychain/keychain.h>

/**
* \brief The CredentialStore class stores user credentials either to QtKeychain
* or QSettings, according to USE_KEYCHAIN cmake flag.
*
* \note Read and write operations are async when QtKeychain is used
*/
class CredentialStore : public QObject
{
Q_OBJECT

public:
explicit CredentialStore( QObject *parent = nullptr );
~CredentialStore() = default;

static const QString KEYCHAIN_GROUP;
static const QString KEYCHAIN_ENTRY_CREDENTIALS;
static const QString KEYCHAIN_ENTRY_TOKEN;

static const QString KEY_USERNAME;
static const QString KEY_PASSWORD;
static const QString KEY_USERID;
static const QString KEY_TOKEN;
static const QString KEY_EXPIRE;

//! Write authentication values data to keychain
void writeAuthData( const QString &username,
const QString &password,
int userId,
const QString &token,
const QDateTime &tokenExpiration );

//! Reads authentication data from keychain and emits a signal with all auth values
void readAuthData();

signals:
//! Emitted when authentication data is read, including all authentication key values
void authDataRead( const QString &username,
const QString &password,
int userId,
const QString &token,
const QDateTime &tokenExpiration );

private:

//! Reads a key from keychain and stores the value in the intermediary results
//! The method recursively calls itself to read both Keychain entries
void readKeyRecursively( const QString &key );

void finishReadingOperation();

QMap<QString, QString> mReadResults; // to store intermediary read results

QKeychain::WritePasswordJob *mWriteJob = nullptr; // owned by this
QKeychain::ReadPasswordJob *mReadJob = nullptr; // owned by this
};

#endif // CREDENTIALSTORE_H
202 changes: 202 additions & 0 deletions core/credentialstorekeychain.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "credentialstore.h"
#include "coreutils.h"

#include <QJsonObject>
#include <QJsonDocument>

const QString CredentialStore::KEYCHAIN_GROUP = QStringLiteral( "mergin_maps" );
const QString CredentialStore::KEYCHAIN_ENTRY_CREDENTIALS = QStringLiteral( "credentials" );
const QString CredentialStore::KEYCHAIN_ENTRY_TOKEN = QStringLiteral( "token" );

const QString CredentialStore::KEY_USERNAME = QStringLiteral( "u" );
const QString CredentialStore::KEY_PASSWORD = QStringLiteral( "p" );
const QString CredentialStore::KEY_USERID = QStringLiteral( "id" );
const QString CredentialStore::KEY_TOKEN = QStringLiteral( "t" );
const QString CredentialStore::KEY_EXPIRE = QStringLiteral( "e" );

CredentialStore::CredentialStore( QObject *parent )
: QObject( parent )
{
mWriteJob = new QKeychain::WritePasswordJob( KEYCHAIN_GROUP, this );
mWriteJob->setAutoDelete( false );

mReadJob = new QKeychain::ReadPasswordJob( KEYCHAIN_GROUP, this );
mReadJob->setAutoDelete( false );
}


void CredentialStore::writeAuthData
( const QString &username,
const QString &password,
int userId,
const QString &token,
const QDateTime &tokenExpiration )
{
//
// 1. Split the data into two jsons
//

QJsonObject credentialsJsonObj;
credentialsJsonObj.insert( KEY_USERNAME, username );
credentialsJsonObj.insert( KEY_PASSWORD, password );
credentialsJsonObj.insert( KEY_USERID, userId );

QJsonDocument credentialsJson( credentialsJsonObj );

QJsonObject tokenJsonObj;
tokenJsonObj.insert( KEY_TOKEN, token );
tokenJsonObj.insert( KEY_EXPIRE, tokenExpiration.toString( Qt::ISODateWithMs ) );

QJsonDocument tokenJson( tokenJsonObj );

//
// 2. Store JSONs one by one
//

mWriteJob->setKey( KEYCHAIN_ENTRY_CREDENTIALS );
mWriteJob->setBinaryData( credentialsJson.toJson( QJsonDocument::Compact ) );

connect( mWriteJob, &QKeychain::Job::finished, this, [this, tokenJson]()
{
if ( mWriteJob->error() )
{
CoreUtils::log( "Auth", QString( "Keychain write error (%1): %2" ).arg( KEYCHAIN_ENTRY_CREDENTIALS, mWriteJob->errorString() ) );
return; // do not try to store the token either
}

// let's store the token now
mWriteJob->setKey( KEYCHAIN_ENTRY_TOKEN );
mWriteJob->setBinaryData( tokenJson.toJson( QJsonDocument::Compact ) );

mWriteJob->start();

}, Qt::SingleShotConnection );

mWriteJob->start();

//
// 3. Clear any previous data from QSettings (migration from the previous QSettings)
//

// TODO: pass
}

void CredentialStore::readAuthData()
{
mReadResults.clear();
readKeyRecursively( KEYCHAIN_ENTRY_CREDENTIALS );
}

void CredentialStore::readKeyRecursively( const QString &key )
{
//
// 1. Read both entries from keychain (async)
//

mReadJob->setKey( key );

connect( mReadJob, &QKeychain::Job::finished, this, [this, key]()
{
if ( mReadJob->error() )
{
CoreUtils::log( "Auth", QString( "Keychain read error: %1" ).arg( mReadJob->errorString() ) );
emit authDataRead( QString(), QString(), -1, QString(), QDateTime() );
return;
}

mReadResults[ key ] = mReadJob->textData();

if ( key == KEYCHAIN_ENTRY_CREDENTIALS )
{
readKeyRecursively( KEYCHAIN_ENTRY_TOKEN ); // Read the second entry
}
else if ( key == KEYCHAIN_ENTRY_TOKEN )
{
finishReadingOperation(); // We have all the data now, let's wrap it up and return back
}

}, Qt::SingleShotConnection );

mReadJob->start();
}

void CredentialStore::finishReadingOperation()
{
//
// 2. Construct JSONs from the intermediary results and emit the data
//

QString username, password;
int userid = -1;
QByteArray token;
QDateTime tokenExpiration;

if ( mReadResults.size() != 2 )
{
CoreUtils::log( QStringLiteral( "Auth" ),
QString( "Something ugly happened when reading, invalid size of the intermediary results, size:" ).arg( mReadResults.size() )
);
emit authDataRead( username, password, userid, token, tokenExpiration );
return;
}

QString credentialsJsonString = mReadResults.value( KEYCHAIN_ENTRY_CREDENTIALS, QString() );
QString tokenJsonString = mReadResults.value( KEYCHAIN_ENTRY_TOKEN, QString() );

if ( credentialsJsonString.isEmpty() || tokenJsonString.isEmpty() )
{
CoreUtils::log(
QStringLiteral( "Auth" ),
QString( "Something ugly happened when reading, one of the read jsons is empty (%1, %2)" ).arg( credentialsJsonString.length(), tokenJsonString.length() )
);
emit authDataRead( username, password, userid, token, tokenExpiration );
return;
}

QJsonParseError parsingError;
QJsonDocument credentialsJson = QJsonDocument::fromJson( credentialsJsonString.toUtf8(), &parsingError );

if ( parsingError.error != QJsonParseError::NoError )
{
CoreUtils::log( QStringLiteral( "Auth" ), QString( "Could not construct credentials JSON when reading, error: %1" ).arg( parsingError.errorString() ) );
emit authDataRead( username, password, userid, token, tokenExpiration );
return;
}

QJsonDocument tokenJson = QJsonDocument::fromJson( tokenJsonString.toUtf8(), &parsingError );

if ( parsingError.error != QJsonParseError::NoError )
{
CoreUtils::log( QStringLiteral( "Auth" ), QString( "Could not construct token JSON when reading, error: %1" ).arg( parsingError.errorString() ) );
emit authDataRead( username, password, userid, token, tokenExpiration );
return;
}

QJsonObject credentialsJsonObject = credentialsJson.object();
QJsonObject tokenJsonObject = tokenJson.object();

username = credentialsJsonObject.value( KEY_USERNAME ).toString();
password = credentialsJsonObject.value( KEY_PASSWORD ).toString();
userid = credentialsJsonObject.value( KEY_USERID ).toInt();

token = tokenJsonObject.value( KEY_TOKEN ).toString().toUtf8();
tokenExpiration = QDateTime::fromString( tokenJsonObject.value( KEY_EXPIRE ).toString(), Qt::ISODateWithMs );

//
// If credentials are empty, we should look at QSettings as we previously stored credentials there.
// Freshly upgraded app might not have the auth data migrated yet.
//
// TODO: pass...
//

emit authDataRead( username, password, userid, token, tokenExpiration );
}
Loading

2 comments on commit d827818

@inputapp-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS - version 25.2.704411 just submitted!

@inputapp-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS - version 25.2.704412 just submitted!

Please sign in to comment.