diff --git a/.gitmodules b/.gitmodules index 47ddbbf..b73ec33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "libs/libgit2"] path = libs/libgit2 url = https://github.com/libgit2/libgit2 +[submodule "libs/git/openssl"] + path = libs/git/openssl + url = https://github.com/openssl/openssl diff --git a/CMakeLists.txt b/CMakeLists.txt index 7863cae..a816b64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ install( PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE DESTINATION ${BIN_DIR} ) +install(FILES /usr/lib/${ARCH_TRIPLET}/libssl.so DESTINATION /lib/${ARCH_TRIPLET}) +install(FILES /usr/lib/${ARCH_TRIPLET}/libcrypto.so DESTINATION /lib/${ARCH_TRIPLET}) # Translations file(GLOB_RECURSE I18N_SRC_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/po qml/*.qml qml/*.js) diff --git a/clickable.json b/clickable.json index b46ff8d..746dbfa 100644 --- a/clickable.json +++ b/clickable.json @@ -21,7 +21,10 @@ "libgit2": { "template": "cmake", "make_jobs": 4, - "build_args": "-DBUILD_SHARED_LIBS=OFF" + "build_args": "-DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS=-fPIC" } - } + }, + "dependencies_target": [ + "libssl-dev" + ] } diff --git a/libs/git/CMakeLists.txt b/libs/git/CMakeLists.txt new file mode 100644 index 0000000..f8f6948 --- /dev/null +++ b/libs/git/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.5.1) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +include(${CMAKE_ROOT}/Modules/ExternalProject.cmake) + +execute_process( + COMMAND dpkg-architecture -qDEB_HOST_MULTIARCH + OUTPUT_VARIABLE ARCH_TRIPLET + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +execute_process ( + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/clean.sh ${ARCH_TRIPLET} +) + +set(THIRD_PATH "${CMAKE_CURRENT_SOURCE_DIR}") +set(OPENSSL_PATH "${THIRD_PATH}/libgit2") +set(LIBGIT2_PATH "${THIRD_PATH}/openssl/") + + +ExternalProject_Add( + OPENSSL_PATH + INSTALL_DIR ${EXTERNAL_LIBS} + DOWNLOAD_COMMAND "" + SOURCE_DIR ${OPENSSL_PATH} + CONFIGURE_COMMAND /configure + BUILD_COMMAND make + INSTALL_COMMAND make install +) + +ExternalProject_Add( + LIBGIT2_PATH + INSTALL_DIR ${EXTERNAL_LIBS} + DOWNLOAD_COMMAND "" + SOURCE_DIR ${LIBGIT2_PATH} +) diff --git a/libs/git/openssl b/libs/git/openssl new file mode 160000 index 0000000..894da2f --- /dev/null +++ b/libs/git/openssl @@ -0,0 +1 @@ +Subproject commit 894da2fb7ed5d314ee5c2fc9fd2d9b8b74111596 diff --git a/plugins/Git/CMakeLists.txt b/plugins/Git/CMakeLists.txt index 74c5426..6a50b0c 100644 --- a/plugins/Git/CMakeLists.txt +++ b/plugins/Git/CMakeLists.txt @@ -30,7 +30,9 @@ INCLUDE_DIRECTORIES(${EXTERNAL_LIBS}/include) add_library(libgit2 STATIC IMPORTED) set_property(TARGET libgit2 PROPERTY IMPORTED_LOCATION "${EXTERNAL_LIBS}/lib/libgit2.a") -target_link_libraries(${PLUGIN} libgit2) + +find_package(OpenSSL REQUIRED) +target_link_libraries(${PLUGIN} libgit2 ${OPENSSL_LIBRARIES}) set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") diff --git a/plugins/Git/git.cpp b/plugins/Git/git.cpp index 96134c8..5c1bacf 100644 --- a/plugins/Git/git.cpp +++ b/plugins/Git/git.cpp @@ -1,10 +1,68 @@ #include #include +#include +#include + +extern "C" { #include - +} #include "git.h" +#include "passphraseprovider.h" -Git::Git() {}; +Git::Git() { + git_libgit2_init(); +}; +Git::~Git() { + git_libgit2_shutdown(); +}; + +bool Git::clone(QUrl url, QString dir_out_path) { + auto ret = false; + auto tmp_dir_path = QStandardPaths::writableLocation( + QStandardPaths::CacheLocation).append("/clone"); + + auto gitCred = new UTGitCredProvider(); + pt2cred_acquire_cb = gitCred->cred_acquire_cb; + + QDir tmp_dir(tmp_dir_path); + tmp_dir.removeRecursively(); + + git_repository *cloned_repo = NULL; + git_clone_options clone_opts = GIT_CLONE_OPTIONS_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE; + clone_opts.checkout_opts = checkout_opts; + clone_opts.fetch_opts.callbacks.credentials = gitCred->*pt2cred_acquire_cb; + + qDebug() << "Cloning " << url << " in " << tmp_dir_path; + auto error = git_clone(&cloned_repo, url.toString().toLocal8Bit().constData(), tmp_dir_path.toLocal8Bit().constData(), &clone_opts); + if (cloned_repo) { + git_repository_free(cloned_repo); + } + if(error) { + const git_error *err = giterr_last(); + if (err) { + qDebug() << "ERROR " << err->klass << ": " << err->message; + } + else { + qDebug() << "ERROR " << error << ": no detailed info"; + } + } + else { + qDebug() << "Removing destination"; + QDir dir_out(dir_out_path); + dir_out.removeRecursively(); + + qDebug() << "Moving cloned dir to destination"; + QDir dir; + qDebug() << tmp_dir_path << " to " << dir_out_path; + ret = dir.rename(tmp_dir_path, dir_out_path); + } + tmp_dir.removeRecursively(); + delete gitCred; + return !error and ret; +} diff --git a/plugins/Git/git.h b/plugins/Git/git.h index 59b4c48..9929ec6 100644 --- a/plugins/Git/git.h +++ b/plugins/Git/git.h @@ -10,7 +10,9 @@ class Git : public QObject public: Git(); - ~Git() override = default; + ~Git(); + + Q_INVOKABLE bool clone(QUrl clone_url, QString dir_out); }; diff --git a/plugins/Git/passphraseprovider.h b/plugins/Git/passphraseprovider.h new file mode 100644 index 0000000..bea4744 --- /dev/null +++ b/plugins/Git/passphraseprovider.h @@ -0,0 +1,77 @@ +#ifndef UTGITCREDPROVIDER_H +#define UTGITCREDPROVIDER_H + +class UTGitCredProvider : public QObject, public PassphraseProvider +{ + Q_OBJECT +private: + std::unique_ptr m_loop; + std::unique_ptr m_sem; + char *m_user; + char *m_password; + bool m_canceled; + +public slots: + void handleResponse(bool canceled, QString user, QString password) + { + if (!canceled) { + gpgrt_asprintf(&m_user, "%s", user.toUtf8().constData()); + gpgrt_asprintf(&m_passphrase, "%s", password.toUtf8().constData()); + } + else + m_canceled = true; + m_loop->quit(); + }; + + +public: + UTGitCredProvider(): + m_loop(std::unique_ptr(new QEventLoop)), + m_sem(std::unique_ptr(new QSemaphore(1))), + m_user(nullptr), + m_password(nullptr), + m_canceled(false) + {} + + int cred_acquire_cb(git_cred **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) { + if (!m_sem->tryAcquire(1, 3000)) + { + qWarning() << "Cannot acquire UTGitCredProvider semaphore."; + canceled = true; + return nullptr; + } + + m_passphrase = nullptr; + m_canceled = false; + + + qDebug() << "Call the QML Dialog Cred Provider"; + QMetaObject::invokeMethod( + Gpg::instance()->getWindow(), "callCredDialog", + Q_ARG(QVariant, username_from_url) + ); + + qDebug() << "Waiting for response"; + + QObject::connect( + Gpg::instance()->getWindow(), SIGNAL(responseCredDialog(bool, QString, QString)), + this, SLOT(handleResponse(bool, QString, QString)) + ); + m_loop->exec(); + + qDebug() << "Set Cred"; + error = git_cred_userpass_plaintext_new(out, username, password); + + qDebug() << "Clean"; + if (m_passphrase) free(m_passphrase); + if (m_user) free(m_user); + m_canceled = false; + m_sem->release(1); + return ret; + }; +}; +#endif diff --git a/po/utpass.qrouland.pot b/po/utpass.qrouland.pot index e841675..2a8e517 100644 --- a/po/utpass.qrouland.pot +++ b/po/utpass.qrouland.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: utpass.qrouland\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-09-21 14:01+0000\n" +"POT-Creation-Date: 2019-09-25 17:36+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -103,63 +103,67 @@ msgid "" "No password found
You can import a password store zip in the settings" msgstr "" -#: ../qml/pages/settings/ImportKeyFile.qml:17 +#: ../qml/pages/settings/gpg/ImportKeyFile.qml:17 msgid "GPG Key Import" msgstr "" -#: ../qml/pages/settings/ImportKeyFile.qml:69 +#: ../qml/pages/settings/gpg/ImportKeyFile.qml:69 msgid "Key import failed !" msgstr "" -#: ../qml/pages/settings/ImportKeyFile.qml:76 +#: ../qml/pages/settings/gpg/ImportKeyFile.qml:76 msgid "Key successfully imported !" msgstr "" -#: ../qml/pages/settings/ImportZip.qml:17 +#: ../qml/pages/settings/gpg/InfoKeys.qml:16 +msgid "Info Keys" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:44 +msgid "Key id : %1" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:49 +msgid "Delete this key" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:68 +msgid "You're are about to delete
%1
Continue ?" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:71 +msgid "%1
will be definitively removed.
Continue ?" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:87 +msgid "Key removal failed !" +msgstr "" + +#: ../qml/pages/settings/gpg/InfoKeys.qml:94 +msgid "Key successfully deleted !" +msgstr "" + +#: ../qml/pages/settings/passwordstore/ImportGit.qml:13 +msgid "Git clone" +msgstr "" + +#: ../qml/pages/settings/passwordstore/ImportZip.qml:17 msgid "Zip Password Store Import" msgstr "" -#: ../qml/pages/settings/ImportZip.qml:72 +#: ../qml/pages/settings/passwordstore/ImportZip.qml:72 msgid "" "Importing a new zip will delete
any existing password store!
Continue ?" msgstr "" -#: ../qml/pages/settings/ImportZip.qml:82 +#: ../qml/pages/settings/passwordstore/ImportZip.qml:82 msgid "Password store import failed !" msgstr "" -#: ../qml/pages/settings/ImportZip.qml:89 +#: ../qml/pages/settings/passwordstore/ImportZip.qml:89 msgid "Password store sucessfully imported !" msgstr "" -#: ../qml/pages/settings/InfoKeys.qml:16 -msgid "Info Keys" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:44 -msgid "Key id : %1" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:49 -msgid "Delete this key" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:68 -msgid "You're are about to delete
%1
Continue ?" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:71 -msgid "%1
will be definitively removed.
Continue ?" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:87 -msgid "Key removal failed !" -msgstr "" - -#: ../qml/pages/settings/InfoKeys.qml:94 -msgid "Key successfully deleted !" -msgstr "" - #: ../qml/pages/settings/Settings.qml:28 msgid "GPG" msgstr "" @@ -180,6 +184,14 @@ msgstr "" msgid "Import a Password Store Zip" msgstr "" -#: ../qml/pages/settings/Settings.qml:56 +#: ../qml/pages/settings/Settings.qml:51 +msgid "Import a Password Store Git" +msgstr "" + +#: ../qml/pages/settings/Settings.qml:60 msgid "Warning: importing delete any exiting Password Store" msgstr "" + +#: ../qml/pages/settings/Settings.qml:67 +msgid "Git" +msgstr "" diff --git a/qml/Main.qml b/qml/Main.qml index 776d424..75930a9 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -38,6 +38,24 @@ MainView { passphraseDialog.validated.connect(validated) passphraseDialog.canceled.connect(canceled) } + + function callCredDialog() { + //TODO add parameters to impove passphrase dialog + var credDialog = PopupUtils.open( + Qt.resolvedUrl("dialogs/CredDialog.qml")) + credDialog.activateFocus() + + var validated = function (user, password) { + responseCredDialog(false, user, password) + } + + var canceled = function () { + responseCredDialog(true, "", "") + } + + credDialog.validated.connect(validated) + credDialog.canceled.connect(canceled) + } PageStack { id: pageStack diff --git a/qml/dialogs/CredDialog.qml b/qml/dialogs/CredDialog.qml new file mode 100644 index 0000000..12de008 --- /dev/null +++ b/qml/dialogs/CredDialog.qml @@ -0,0 +1,63 @@ +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 + +Dialog { + id: credProvider + title: i18n.tr("Authentication required") + text_user: i18n.tr("Enter user :") + text_password: i18n.tr("Enter password :") + + signal validated(string user, string password) + signal canceled + + function activateFocus() { + user.forceActiveFocus() + } + + TextField { + id: userField + + placeholderText: i18n.tr("user") + echoMode: TextInput.Password + + onAccepted: passwordField.forceActiveFocus() + } + + TextField { + id: passwordField + + placeholderText: i18n.tr("password") + echoMode: TextInput.Password + + onAccepted: okButton.clicked(text) + } + + Button { + id: okButton + + text: i18n.tr("Ok") + color: UbuntuColors.green + + onClicked: { + validated(userField.text, passwordField.text) + userField.text = "" + passwordField.text = "" + PopupUtils.close(credProvider) + } + } + + Button { + id: cancelButton + text: i18n.tr("Cancel") + + color: UbuntuColors.red + + onClicked: { + userField.text = "" + passwordField.text = "" + canceled() + PopupUtils.close(credProvider) + } + } +} diff --git a/qml/dialogs/PassphraseDialog.qml b/qml/dialogs/PassphraseDialog.qml index 553e1c3..3c97978 100644 --- a/qml/dialogs/PassphraseDialog.qml +++ b/qml/dialogs/PassphraseDialog.qml @@ -43,6 +43,7 @@ Dialog { color: UbuntuColors.red onClicked: { + passphraseField.text = "" canceled() PopupUtils.close(passphraseProvider) } diff --git a/qml/pages/settings/Settings.qml b/qml/pages/settings/Settings.qml index 7716463..3d5e0fe 100644 --- a/qml/pages/settings/Settings.qml +++ b/qml/pages/settings/Settings.qml @@ -28,11 +28,11 @@ Page { text: i18n.tr('GPG') } PageStackLink { - page: Qt.resolvedUrl("ImportKeyFile.qml") + page: Qt.resolvedUrl("gpg/ImportKeyFile.qml") text: i18n.tr('Import a GPG key file') } PageStackLink { - page: Qt.resolvedUrl("InfoKeys.qml") + page: Qt.resolvedUrl("gpg/InfoKeys.qml") text: i18n.tr('Show GPG keys') } Text { @@ -43,9 +43,13 @@ Page { text: i18n.tr('Password Store') } PageStackLink { - page: Qt.resolvedUrl("ImportZip.qml") + page: Qt.resolvedUrl("passwordstore/ImportZip.qml") text: i18n.tr('Import a Password Store Zip') } + PageStackLink { + page: Qt.resolvedUrl("passwordstore/ImportGit.qml") + text: i18n.tr('Import a Password Store Git') + } Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -55,5 +59,12 @@ Page { text: i18n.tr( 'Warning: importing delete any exiting Password Store') } + Text { + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + width: parent.width + height: units.gu(4) + text: i18n.tr('Git') + } } } diff --git a/qml/pages/settings/ImportKeyFile.qml b/qml/pages/settings/gpg/ImportKeyFile.qml similarity index 97% rename from qml/pages/settings/ImportKeyFile.qml rename to qml/pages/settings/gpg/ImportKeyFile.qml index ec6d8db..f445f2f 100644 --- a/qml/pages/settings/ImportKeyFile.qml +++ b/qml/pages/settings/gpg/ImportKeyFile.qml @@ -4,8 +4,8 @@ import Ubuntu.Content 1.3 import Ubuntu.Components.Popups 1.3 import Pass 1.0 import Utils 1.0 -import "../headers" -import "../../dialogs" +import "../../headers" +import "../../../dialogs" Page { id: importKeyFilePage diff --git a/qml/pages/settings/InfoKeys.qml b/qml/pages/settings/gpg/InfoKeys.qml similarity index 97% rename from qml/pages/settings/InfoKeys.qml rename to qml/pages/settings/gpg/InfoKeys.qml index bf4ba81..242193e 100644 --- a/qml/pages/settings/InfoKeys.qml +++ b/qml/pages/settings/gpg/InfoKeys.qml @@ -2,9 +2,9 @@ import QtQuick 2.4 import Ubuntu.Components 1.3 import Ubuntu.Components.Popups 1.3 import Pass 1.0 -import "../headers" -import "../../components" -import "../../dialogs" +import "../../headers" +import "../../../components" +import "../../../dialogs" Page { id: infoKeysPage diff --git a/qml/pages/settings/passwordstore/ImportGit.qml b/qml/pages/settings/passwordstore/ImportGit.qml new file mode 100644 index 0000000..21eb955 --- /dev/null +++ b/qml/pages/settings/passwordstore/ImportGit.qml @@ -0,0 +1,61 @@ +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Git 1.0 +import Pass 1.0 +import "../../headers" +import "../../../styles" + +Page { + id: importGit + + header: StackHeader { + id: importGitHeader + title: i18n.tr('Git clone') + } + + Row { + anchors.top: importGitHeader.bottom + anchors.topMargin: units.gu(1) + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + height: units.gu(4) + + Rectangle { + height: units.gu(4) + width: units.gu(2) + } + TextField { + id: gitUrlTextField + placeholderText: "https://..." + height: units.gu(4) + width: parent.width - units.gu(8) + } + + Icon { + id: ico + name: "document-save" + color: UbuntuColors.orange + height: units.gu(4) + width: units.gu(4) + + MouseArea { + anchors.fill: parent + onPressed: { + parent.color = UbuntuColors.warmGrey + } + onClicked: { + Git.clone(gitUrlTextField.text, Pass.getPasswordStore()) + } + onReleased: { + parent.color = theme.palette.normal.background + } + } + } + } + + Component.onCompleted : { + gitUrlTextField.forceActiveFocus() + } +} + diff --git a/qml/pages/settings/ImportZip.qml b/qml/pages/settings/passwordstore/ImportZip.qml similarity index 98% rename from qml/pages/settings/ImportZip.qml rename to qml/pages/settings/passwordstore/ImportZip.qml index b047a4b..80bfc35 100644 --- a/qml/pages/settings/ImportZip.qml +++ b/qml/pages/settings/passwordstore/ImportZip.qml @@ -4,8 +4,8 @@ import Ubuntu.Content 1.3 import Ubuntu.Components.Popups 1.3 import Pass 1.0 import Utils 1.0 -import "../headers" -import "../../dialogs" +import "../../headers" +import "../../../dialogs" Page { id: importZipPage