1
0
mirror of https://github.com/QRouland/UTPass.git synced 2025-07-04 19:22:27 +00:00

46 Commits

Author SHA1 Message Date
2be75f9bd6 Fix typo 2025-03-14 10:09:20 +01:00
dc2c35ca9b Improve Lib Pass Error Messages 2025-03-14 10:07:05 +01:00
bdb2d58ac4 Some improvements 2025-03-12 15:34:33 +01:00
884488b9ed Add support for ssh clone 2025-02-21 15:50:27 +01:00
5683db69c7 Update README 2025-02-18 16:04:47 +01:00
6306af6dde Add initial ssh support in git cpp plugin 2025-02-18 14:20:35 +01:00
ec4ebca950 Merge pull request #4 from Vistaus/master
Added Dutch translation
2025-02-17 07:41:10 +00:00
cf89bd04bb Added Dutch translation 2025-02-12 22:21:11 +01:00
7e8ac60cc9 Some clean up 2025-02-11 10:52:11 +01:00
c6f2424017 Fix issue where textfield used wrong signals 2025-02-07 14:17:14 +01:00
6fe50d2c90 Save last git clone settings (urls,types) 2025-02-07 13:38:09 +01:00
2409f33f59 Add search password feature (equivalent to pass find) 2025-02-05 15:07:38 +01:00
e47d50072a Fix style command to not include third party libs 2025-02-05 15:07:38 +01:00
197a12a570 Typos Changelog 2025-02-05 15:07:31 +01:00
50441e0daf Typos Changelog 2025-02-04 18:21:35 +01:00
6c93aa3d62 Bump version 0.0.4-dev 2025-02-04 18:20:41 +01:00
0cf07b1b7a Bump version 0.0.3 2025-02-04 17:51:14 +01:00
6b5d9fb8e2 Fix #2 Password names containing periods are truncated 2025-02-04 17:44:42 +01:00
c106bbec19 Fix main page not reloading on imports 2025-02-04 17:27:01 +01:00
bcc6d7c316 Update Readme 2025-02-04 17:25:37 +01:00
fc02000b89 Fix decrypt issue 2025-02-04 16:36:00 +01:00
72a3a8fbcc Fix password page color issues with dark theme 2025-02-04 16:17:28 +01:00
efb57dd70c Fix ui issues on imports 2025-02-04 15:51:47 +01:00
bef910bce3 Update readme for export with new git functionnality 2025-02-04 14:25:31 +01:00
bf7c2091b7 Fix bug causing crashes during git clone when no username is provided 2025-02-04 14:09:02 +01:00
5582b4dd70 Ensure that key and zip are deleted after import 2025-02-04 13:37:25 +01:00
ba52ddac5c Complete rewrite from gpgme to rnp 2025-02-03 21:46:21 +01:00
e56c16f27b Fix rnp passphrase provider version 2025-02-03 20:06:15 +01:00
93361f9ba5 Fix build rnp for arm64 2025-02-03 17:48:30 +01:00
b9b038b1ae Rewrite get all key with rnp 2025-02-01 14:12:18 +01:00
74a001eefc WIP Rewrite get all key with rnp 2025-01-30 22:46:46 +01:00
4bec3dcbc9 First draft Rewrite get all key with rnp 2025-01-30 16:25:29 +01:00
2c9d82e0b1 Rewrite import key with rnp 2025-01-29 16:42:37 +01:00
c01fae0c58 Add rnp lib 2025-01-28 12:45:31 +01:00
630d707190 Some ui improvements 2025-01-28 10:17:57 +01:00
7a2b12419d Refactor unzip password store 2025-01-20 15:58:34 +01:00
20aff3a404 Some bugs fix 2025-01-20 15:03:34 +01:00
ebfc6f500d Add delete password store feature 2025-01-20 14:46:54 +01:00
0eb8920856 Fix delete gpg key 2025-01-20 11:24:38 +01:00
c0757da47b Refactor cloning feature and ui 2025-01-17 10:40:54 +01:00
beaad58af2 Add docsting for pass plugin 2025-01-15 23:40:35 +01:00
e589abd10c Refactor gpg for more clean job handling 2025-01-15 23:15:00 +01:00
00116aea8c Fix am issue with page stack titles when navigationg the password list 2025-01-14 14:02:13 +01:00
fd3ab95b27 Style 2025-01-14 13:05:09 +01:00
200964246e Improve gpg key infos 2025-01-14 12:20:55 +01:00
0e5df76787 Harmonize naming 2025-01-14 09:52:49 +01:00
93 changed files with 4476 additions and 1187 deletions

6
.gitmodules vendored Normal file
View File

@ -0,0 +1,6 @@
[submodule "libs/rnp"]
path = libs/rnp
url = https://github.com/rnpgp/rnp
[submodule "libs/botan"]
path = libs/botan
url = https://github.com/randombit/botan

24
CHANGELOG.md Normal file
View File

@ -0,0 +1,24 @@
# 0.0.3: Git Initial Support and Move to RNP
- Port app to Focal
- Improve UI :
* Follow human interface guidelines
* Fix various components color to work with dark theme
- Rewrite of Pass Plugin:
* Move from GPGMe to RNP for GPG operations due to issues running GPG agent in a confined app
* Improve multithreading code for GPG operations
- Add Git HTTP and HTTP AUTH clone for password store import feature
- Add delete password store feature
# 0.0.2 : Added translations
- Added French by Anne17 and Reda
- Added Catalan by Joan CiberSheep
- Added Spanish by Advocatux and Reda
Thanks to all the translators !
# 0.0.1 : Initial Release
- Import of gpg keys via file
- Suppression of gpg keys
- Import of password via a password store zip
- Password decryption
- Password copy to clipboard

View File

@ -39,11 +39,19 @@ if(NOT TESTS_PATH)
set(TESTS_PATH "./tests") set(TESTS_PATH "./tests")
endif() endif()
configure_file(main.in.cpp main.cpp) if(TESTS_RUNNER)
configure_file(tests.in.cpp tests.cpp)
add_executable(${PROJECT_NAME} tests.cpp)
else()
add_executable(${PROJECT_NAME} main.cpp)
endif()
add_executable(${PROJECT_NAME} main.cpp)
qt5_use_modules(${PROJECT_NAME} Gui Qml Quick QuickTest) qt5_use_modules(${PROJECT_NAME} Gui Qml Quick QuickTest)
set_source_files_properties(qml/singletons/GitSettings.qml PROPERTIES
QT_QML_SINGLETON_TYPE TRUE
)
if(TESTS_RUNNER) if(TESTS_RUNNER)
qt5_use_modules(${PROJECT_NAME} QuickTest) qt5_use_modules(${PROJECT_NAME} QuickTest)
endif() endif()

View File

@ -4,7 +4,7 @@ A Ubuntu Touch password management app aiming to be compatible with [ZX2C4s p
## Installation ## Installation
UTPass is avalaible on the [OpenStore](open-store.io) UTPass is avalaible on the [OpenStore](https://open-store.io)
[![OpenStore](https://open-store.io/badges/en_US.png)](https://open-store.io/app/utpass.qrouland) [![OpenStore](https://open-store.io/badges/en_US.png)](https://open-store.io/app/utpass.qrouland)
@ -20,22 +20,25 @@ Assuming that there are already passwords in another device using [ZX2C4s pas
Export gpg private keys in order to decrypt passwords: Export gpg private keys in order to decrypt passwords:
``` ```
gpg --output keys.gpg --export-secret-keys gpg --output keys.gpg --export-secret-keys <key>
``` ```
Export passwords, assuming they reside in *.password-store* folder: If your password store is already hosted in a Git repository that provides HTTP or HTTP with authentication for cloning (we're working to have support for SSH soon), you can clone your password store directly from the app.
Otherwise, follow these steps to export it to a ZIP file for importing.
Export passwords to a ZIP archive, assuming they reside in the *.password-store* folder:
``` ```
zip passwords.zip -r .password-store/ zip passwords.zip -r .password-store/
``` ```
Both files have the correct format for UTPass to import them and work as intended. It is highly recommended to remove them after imported to **UTPass**. Files have the correct format for UTPass to import them and work as intended. It is highly recommended to remove them after imported to **UTPass**.
## Build & Tests ## Build & Tests
See [Build & Tests wiki page](https://github.com/QRouland/UTPass/wiki/Build-&-Tests) See [Build & Tests wiki page](https://github.com/QRouland/UTPass/wiki/Build-&-Tests)
## Contributing & Issues ## Contributing & Translation
See [Contributing wiki page](https://github.com/QRouland/UTPass/wiki/Contributing) See [Contributing wiki page](https://github.com/QRouland/UTPass/wiki/Contributing)
@ -44,11 +47,9 @@ See [Contributing wiki page](https://github.com/QRouland/UTPass/wiki/Contributin
Some useful links related to UTPass development : Some useful links related to UTPass development :
* [Ubports](https://ubports.com/) : Ubuntu Touch Community * [Ubports](https://ubports.com/) : Ubuntu Touch Community
* [ZX2C4s pass command line application](https://www.passwordstore.org/) : the standard unix password manager. * [ZX2C4s pass command line application](https://www.passwordstore.org/) : The standard unix password manager.
* [Clickable](https://github.com/bhdouglass/clickable) : Compile, build, and deploy Ubuntu Touch click packages * [Clickable](https://github.com/bhdouglass/clickable) : Compile, build, and deploy Ubuntu Touch click packages
* [GnuPG](https://gnupg.org/): The GNU Privacy Guard * [Rnp](https://github.com/rnpgp/rnp) : High performance C++ OpenPGP library used by Mozilla Thunderbird
* [Gpgme](https://www.gnupg.org/software/gpgme/index.html) : GnuPG Made Easy (GPGME) is a library designed to make access to GnuPG easier for applications
## License ## License

View File

@ -4,31 +4,41 @@ kill: UTPass
scripts: scripts:
style: >- style: >-
echo 'Running Astyle :' && astyle --options=.astylerc --recursive '*.cpp,*.h' --exclude=build && echo 'Running QmlFormat' && find . -name "*.qml" -exec qmlformat -i {} \; && echo 'Success' echo 'Running Astyle :' && astyle --options=.astylerc --recursive '*.cpp,*.h' --exclude=build --exclude=libs && echo 'Running QmlFormat' && find . -name "*.qml" -exec qmlformat -i {} \; && echo 'Success'
dependencies_target: dependencies_target:
- libgpgmepp-dev
- libgpgme-dev
- libgit2-dev - libgit2-dev
- libquazip5-dev - libquazip5-dev
- gpg - libjson-c-dev
libraries:
botan:
builder: custom
make_jobs: 2
dependencies_host:
- python
prebuild:
- $SRC_DIR/configure.py --cpu $ARCH --prefix $INSTALL_DIR --with-build-dir $BUILD_DIR
build:
- make
- make install
rnp:
builder: cmake
make_jobs: 2
dependencies_target:
- libbz2-dev
- zlib1g-dev
- libjson-c-dev
build_args: -DBUILD_TESTING=off -DCRYPTO_BACKEND=botan
install_lib: install_lib:
- "libgpg-error.so.0.28.0"
- "libassuan.so*"
- "libgpgme.so*"
- "libgpgmepp.so*"
- "libqgpgme.so*"
- "libgit2.so*" - "libgit2.so*"
- "libquazip5.so*"
- "libmbedtls.so*" - "libmbedtls.so*"
- "libmbedx509.so*" - "libmbedx509.so*"
- "libmbedcrypto.so*" - "libmbedcrypto.so*"
- "libhttp_parser.so*" - "libhttp_parser.so*"
- "libssh2.so*" - "libssh2.so*"
- "libquazip5.so*"
install_bin:
- "gpg"

127
cmake/FindJSON-C.cmake Normal file
View File

@ -0,0 +1,127 @@
# Copyright (c) 2018, 2024 Ribose Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#.rst:
# FindJSON-C
# -----------
#
# Find the json-c library.
#
# IMPORTED Targets
# ^^^^^^^^^^^^^^^^
#
# This module defines :prop_tgt:`IMPORTED` targets:
#
# ``JSON-C::JSON-C``
# The json-c library, if found.
#
# Result variables
# ^^^^^^^^^^^^^^^^
#
# This module defines the following variables:
#
# ::
#
# JSON-C_FOUND - true if the headers and library were found
# JSON-C_INCLUDE_DIRS - where to find headers
# JSON-C_LIBRARIES - list of libraries to link
# JSON-C_VERSION - library version that was found, if any
# use pkg-config to get the directories and then use these values
# in the find_path() and find_library() calls
find_package(PkgConfig)
pkg_check_modules(PC_JSON-C QUIET json-c)
# RHEL-based systems may have json-c12
if (NOT PC_JSON-C_FOUND)
pkg_check_modules(PC_JSON-C QUIET json-c12)
endif()
# ..or even json-c13, accompanied by non-develop json-c (RHEL 8 ubi)
if (NOT PC_JSON-C_FOUND)
pkg_check_modules(PC_JSON-C QUIET json-c13)
endif()
# find the headers
find_path(JSON-C_INCLUDE_DIR
NAMES json_c_version.h
HINTS
${PC_JSON-C_INCLUDEDIR}
${PC_JSON-C_INCLUDE_DIRS}
PATH_SUFFIXES json-c json-c12 json-c13
)
# find the library
find_library(JSON-C_LIBRARY
NAMES json-c libjson-c json-c12 libjson-c12 json-c13 libjson-c13
HINTS
${PC_JSON-C_LIBDIR}
${PC_JSON-C_LIBRARY_DIRS}
)
# determine the version
if(PC_JSON-C_VERSION)
set(JSON-C_VERSION ${PC_JSON-C_VERSION})
elseif(JSON-C_INCLUDE_DIR AND EXISTS "${JSON-C_INCLUDE_DIR}/json_c_version.h")
file(STRINGS "${JSON-C_INCLUDE_DIR}/json_c_version.h" _json-c_version_h
REGEX "^#define[\t ]+JSON_C_VERSION[\t ]+\"[^\"]*\"$")
string(REGEX REPLACE ".*#define[\t ]+JSON_C_VERSION[\t ]+\"([^\"]*)\".*"
"\\1" _json-c_version_str "${_json-c_version_h}")
set(JSON-C_VERSION "${_json-c_version_str}"
CACHE INTERNAL "The version of json-c which was detected")
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(JSON-C
REQUIRED_VARS JSON-C_LIBRARY JSON-C_INCLUDE_DIR JSON-C_VERSION
VERSION_VAR JSON-C_VERSION
)
if (JSON-C_FOUND)
set(JSON-C_INCLUDE_DIRS ${JSON-C_INCLUDE_DIR} ${PC_JSON-C_INCLUDE_DIRS})
set(JSON-C_LIBRARIES ${JSON-C_LIBRARY})
endif()
if (JSON-C_FOUND AND NOT TARGET JSON-C::JSON-C)
# create the new library target
add_library(JSON-C::JSON-C UNKNOWN IMPORTED)
# set the required include dirs for the target
if (JSON-C_INCLUDE_DIRS)
set_target_properties(JSON-C::JSON-C
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${JSON-C_INCLUDE_DIRS}"
)
endif()
# set the required libraries for the target
if (EXISTS "${JSON-C_LIBRARY}")
set_target_properties(JSON-C::JSON-C
PROPERTIES
IMPORTED_LINK_INTERFACE_LANGUAGES "C"
IMPORTED_LOCATION "${JSON-C_LIBRARY}"
)
endif()
endif()
mark_as_advanced(JSON-C_INCLUDE_DIR JSON-C_LIBRARY)

1
libs/botan Submodule

Submodule libs/botan added at 935055e839

1
libs/rnp Submodule

Submodule libs/rnp added at 2e249423d6

View File

@ -6,10 +6,6 @@
#include <QtQml> #include <QtQml>
#ifdef TEST_RUNNER
#include <QtQuickTest/quicktest.h>
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
qDebug() << "Starting app from main.cpp"; qDebug() << "Starting app from main.cpp";
@ -17,7 +13,6 @@ int main(int argc, char *argv[])
QGuiApplication::setApplicationName("utpass.qrouland"); QGuiApplication::setApplicationName("utpass.qrouland");
#ifndef TEST_RUNNER
auto *view = new QQuickView(); auto *view = new QQuickView();
view->setSource(QUrl(QStringLiteral("qml/Main.qml"))); view->setSource(QUrl(QStringLiteral("qml/Main.qml")));
view->setResizeMode(QQuickView::SizeRootObjectToView); view->setResizeMode(QQuickView::SizeRootObjectToView);
@ -28,7 +23,4 @@ int main(int argc, char *argv[])
Q_ARG(QVariant, QVariant::fromValue(mainView)) Q_ARG(QVariant, QVariant::fromValue(mainView))
); );
return QGuiApplication::exec(); return QGuiApplication::exec();
#else
return quick_test_main(argc, argv, "@TESTS_PATH@", "@TESTS_PATH@");
#endif
} }

View File

@ -10,7 +10,7 @@
"content-hub": "UTPass.contenthub" "content-hub": "UTPass.contenthub"
} }
}, },
"version": "0.0.3-dev", "version": "0.0.4-dev",
"maintainer": "Quentin Rouland <quentin@qrouland.com>", "maintainer": "Quentin Rouland <quentin@qrouland.com>",
"framework" : "ubuntu-sdk-20.04" "framework" : "ubuntu-sdk-20.04"
} }

View File

@ -4,9 +4,10 @@ set(PLUGIN "Git")
set( set(
SRC SRC
plugin.cpp plugin.cpp
libgit.cpp
git.cpp git.cpp
utils.h utils.h
jobs/clonejob.cpp
jobs/gitjob.cpp
) )
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)

View File

@ -2,78 +2,102 @@
#include <QtCore/QDir> #include <QtCore/QDir>
#include <QDebug> #include <QDebug>
#include <QStandardPaths> #include <QStandardPaths>
extern "C" {
#include <git2.h>
}
#include "git.h" #include "git.h"
#include "libgit.h"
#include "utils.h" #include "utils.h"
#include "jobs/clonejob.h"
#include "jobs/gitjob.h"
Git::Git():
QDir Git::clone_setup() m_sem(std::unique_ptr<QSemaphore>(new QSemaphore(1))),
m_ssh_homedir (QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation).append("/.ssh"))
{ {
QDir tmp_dir(QStandardPaths::writableLocation( QStandardPaths::CacheLocation).append("/clone")); qDebug() << "[Git] SSH Home is " << m_ssh_homedir.absolutePath();
QDir m_ssh_homedir(this->m_ssh_homedir);
tmp_dir.removeRecursively(); if (!m_ssh_homedir.exists()) {
qDebug() << "Temp dir path is " << tmp_dir.absolutePath(); m_ssh_homedir.mkpath(".");
return tmp_dir;
}
bool Git::clone_tear_down(QDir tmp_dir)
{
return tmp_dir.removeRecursively();
}
bool Git::move_to_destination(QString path, QDir tmp_dir)
{
qDebug() << "Removing password_store " << path;
QDir destination_dir(path);
destination_dir.removeRecursively();
qDebug() << "Moving cloned content to destination dir";
QDir dir;
qDebug() << tmp_dir.absolutePath() << " to " << destination_dir.absolutePath();
return dir.rename(tmp_dir.absolutePath(), destination_dir.absolutePath()); // TODO Better error handling
}
bool Git::clone(QString url, QString path, mode_type mode) //, GitPlugin::RepoType type, QString pass)
{
auto v = overload {
[](const Unset & x) { return "Unset"; },
[](const HTTP & x) { return "Unset"; },
[](const HTTPAuth & x) { return "HTTPAuth"; },
[](const SSHAuth & x) { return "SSHAuth"; },
[](const SSHKey & x) { return "SSHKey"; },
};
qInfo() << "Cloning " << url << " to destination " << path << " using " << std::visit(v, mode);
LibGit::instance()->set_mode(mode);
auto tmp_dir = this->clone_setup();
qDebug() << "Cloning " << url << " to tmp dir " << tmp_dir.absolutePath();
auto ret = LibGit::instance()->clone(url, tmp_dir.absolutePath()); // TODO Better error handling
if (ret) {
this->move_to_destination(path, tmp_dir);
} }
git_libgit2_init();
this->clone_tear_down(tmp_dir);
LibGit::instance()->set_mode(Unset());
return ret ;
} }
bool Git::clone_http(QString url, QString path) //, GitPlugin::RepoType type, QString pass) Git::~Git()
{ {
git_libgit2_shutdown();
}
bool Git::clone(QString url, QString path, cred_type mode)
{
if (!this->m_sem->tryAcquire(1, 500)) {
qWarning() << "[Git] Can acquire git semaphore a command is already running ";
return false;
}
auto v = overload {
[](const HTTP & x) { UNUSED(x); return "HTTP"; },
[](const HTTPUserPass & x) { UNUSED(x); return "HTTPAuth"; },
[](const SSHKey & x) { UNUSED(x); return "SSHKey"; },
};
qDebug() << "[Git] Creating clone Job " << url << " " << path << " " << std::visit(v, mode);
CloneJob *clone_job = new CloneJob(url, path, mode);
connect(clone_job, &CloneJob::resultReady, this, &Git::cloneResult);
connect(clone_job, &CloneJob::finished, clone_job, &QObject::deleteLater);
clone_job->start();
return true;
}
bool Git::cloneHttp(QString url, QString path)
{
qInfo() << "[Git] Call clone command Http " << url << " " << path;
HTTP mode = {}; HTTP mode = {};
return this->clone(url, path, mode); return this->clone(url, path, mode);
} }
bool Git::clone_http_pass(QString url, QString path, QString pass) bool Git::cloneHttpPass(QString url, QString path, QString pass)
{ {
HTTPAuth mode = { pass }; qInfo() << "[Git] Call clone command HttpPass " << url << " " << path;
HTTPUserPass mode = { pass };
return this->clone(url, path, mode); return this->clone(url, path, mode);
} }
bool Git::cloneSshKey(QString url, QString path, QString passphrase)
{
qInfo() << "[Git] Call clone command SshKey " << url << " " << path;
SSHKey mode = { this->pubKeyPath(), this->privKeyPath(), passphrase };
return this->clone(url, path, mode);
}
void Git::cloneResult(const bool err)
{
if (err) {
emit cloneFailed(); // TODO error message
} else {
emit cloneSucceed();
}
this->m_sem->release();
}
bool Git::importSshKey(QUrl source_path, bool is_private){
auto destination_path = is_private ? this->privKeyPath() : this->pubKeyPath();
QFile source_file(source_path.toLocalFile());
if (!source_file.exists()) {
qWarning() << "[Git] Source file does not exist.";
return false;
}
QDir target_dir = QFileInfo(destination_path).absoluteDir();
if (!target_dir.exists()) {
if (!target_dir.mkpath(".")) {
qWarning() << "[Git] Failed to create target directory.";
return false;
}
}
return source_file.copy(destination_path);
}

View File

@ -1,32 +1,148 @@
#ifndef GIT_H #ifndef GIT_H
#define GIT_H #define GIT_H
#include "jobs/gitjob.h"
#include "qdebug.h"
#include <QUrl> #include <QUrl>
#include <QObject> #include <QObject>
#include <QSemaphore>
#include <QtCore/QDir> #include <QtCore/QDir>
#include "libgit.h" /**
* @class Git
* @brief A class that provides Git functionality for cloning and updating repositories.
*
* The `Git` class provides a set of methods to do Git operation on remote URLs.
*/
class Git : public QObject class Git : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString privKey READ pubKeyPath)
Q_PROPERTY(QString pubKey READ privKeyPath)
private slots:
/**
* @brief Slot that handles the result of a cloning operation.
*
* This slot is connected to the result of the cloning operation and is triggered when the cloning
* process finishes. It emits the appropriate signal based on whether the clone operation succeeded
* or failed.
*
* @param err A boolean indicating whether an error occurred during cloning. `true` if the clone failed,
* `false` if it succeeded.
*/
void cloneResult(const bool err);
signals:
/**
* @brief Signal emitted when the cloning operation succeeds.
*
* This signal is emitted when the Git repository is successfully cloned.
*/
void cloneSucceed();
/**
* @brief Signal emitted when the cloning operation fails.
*
* This signal is emitted when an error occurs during the cloning operation.
*/
void cloneFailed();
private: private:
QDir clone_setup(); std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing concurrent operations. */
bool move_to_destination(QString path, QDir tmp_dir); QDir m_ssh_homedir; /**< Directory that contains the SSH keys (public and private). */
bool clone_tear_down(QDir tmp_dir);
bool clone(QString url, QString path, mode_type mode);
/**
* @brief Clones a repository from a specified URL.
*
* This private method initiates the cloning job. It sets up the repository cloning process based on
* the specified URL, destination path, and cloning mode (HTTP, HTTP_AUTH or SSH).
*
* @param url The URL of the Git repository to clone.
* @param path The destination path for the cloned repository.
* @param mode The cloning mode, such as HTTP or SSH (represented as `cred_type`).
* @return `true` if the cloning process was successful, `false` otherwise.
*/
bool clone(QString url, QString path, cred_type mode);
protected:
/**
* @brief Get the path to the public keyring.
*
* @return The file path to the public key.
*/
QString pubKeyPath()
{
return this->m_ssh_homedir.filePath("id_rsa.pub");
}
/**
* @brief Get the path to the secret keyring.
*
* @return The file path to the private key.
*/
QString privKeyPath()
{
return this->m_ssh_homedir.filePath("id_rsa");
}
public: public:
Git() = default; /**
~Git() override = default; * @brief Constructor for the Git class.
*
* Initializes the `Git` class.
*/
Git();
/**
* @brief Destructor for the Git class.
*
* Cleans up any resources used by the `Git` class.
*/
~Git() override;
Q_INVOKABLE bool importSshKey(QUrl source_path, bool is_private);
/**
* @brief Clones a repository over HTTP.
*
* This method clones a Git repository from the specified HTTP URL and saves it to the given destination path.
*
* @param url The HTTP URL of the Git repository to clone.
* @param path The destination path for the cloned repository.
* @return `true` if the clone operation was successfully started, `false` otherwise.
*/
Q_INVOKABLE bool cloneHttp(QString url, QString path);
/**
* @brief Clones a repository over HTTP with a password for authentication.
*
* This method clones a Git repository from the specified HTTP URL using the provided password for authentication,
* and saves it to the given destination path.
*
* @param url The HTTP URL of the Git repository to clone.
* @param path The destination path for the cloned repository.
* @param pass The password used for HTTP authentication.
* @return `true` if the clone job operation was successfully started, `false` otherwise.
*/
Q_INVOKABLE bool cloneHttpPass(QString url, QString path, QString pass);
/**
* @brief Clones a repository over SSH with a key for authentication.
*
* This method clones a Git repository from the specified ssh URL using the provided password for authentication,
* and saves it to the given destination path.
*
* @param url The HTTP URL of the Git repository to clone.
* @param path The destination path for the cloned repository.
* @param passphrase The passphrase used for SSH authentication.
* @return `true` if the clone job operation was successfully started, `false` otherwise.
*/
Q_INVOKABLE bool cloneSshKey(QString url, QString path, QString passphrase);
Q_INVOKABLE bool clone_http(QString url, QString path);
Q_INVOKABLE bool clone_http_pass(QString url, QString path, QString pass);
// Q_INVOKABLE bool clone_ssh_pass(QString url, QString path, QString pass);
// Q_INVOKABLE bool clone_ssh_key(QString url, QString path, QString pub_key, QString priv_key, QString passphrase);
// Q_INVOKABLE bool update(QUrl url, QString path); // Q_INVOKABLE bool update(QUrl url, QString path);
// ....
}; };
#endif #endif

View File

@ -0,0 +1,86 @@
#include <QStandardPaths>
#include <QDir>
#include <QUrl>
#include <QDebug>
#include <QObject>
#include <type_traits>
extern "C" {
#include <git2.h>
}
#include "clonejob.h"
CloneJob::CloneJob(QString url, QString path, cred_type cred):
GitJob(cred),
m_url(url),
m_path(path)
{
this->setObjectName("CloneJob");
}
void CloneJob::run()
{
auto tmp_dir = this->cloneSetup();
auto ret = this->clone(this->m_url, tmp_dir.absolutePath(), this->m_cred, this->credentialsCB);
if (ret) {
this->moveToDestination(tmp_dir, this->m_path);
}
this->cloneCleanUp(tmp_dir);
emit resultReady(!ret); // TODO Clean error handling to return specifics errors for the ui
}
QDir CloneJob::cloneSetup()
{
QDir tmp_dir(QStandardPaths::writableLocation( QStandardPaths::CacheLocation).append("/clone"));
tmp_dir.removeRecursively();
qDebug() << "[CloneJob] Temp dir path is " << tmp_dir.absolutePath();
return tmp_dir;
}
bool CloneJob::cloneCleanUp(QDir tmp_dir)
{
return tmp_dir.removeRecursively();
}
bool CloneJob::moveToDestination(QDir tmp_dir, QString path)
{
qDebug() << "[CloneJob] Removing password_store " << path;
QDir destination_dir(path);
destination_dir.removeRecursively();
qDebug() << "[CloneJob] Moving cloned content to destination dir";
QDir dir;
qDebug() << "[CloneJob]" << tmp_dir.absolutePath() << " to " << destination_dir.absolutePath();
return dir.rename(tmp_dir.absolutePath(), destination_dir.absolutePath()); // TODO Better error handling
}
bool CloneJob::clone(QString url, QString path, cred_type cred, git_cred_acquire_cb cb)
{
git_repository *repo = NULL;
git_clone_options opts = GIT_CLONE_OPTIONS_INIT;
PayloadCB payload = PayloadCB(false, cred);
opts.fetch_opts.callbacks.credentials = cb;
opts.fetch_opts.callbacks.payload = &payload;
int ret = git_clone(&repo, url.toLocal8Bit().data(), path.toLocal8Bit().data(), &opts);
if (ret == GIT_EUSER ) {
qDebug() << "[CloneJob] CallBack Error";
} else if (ret != 0) {
auto err = git_error_last(); // TODO Better error handling for return ui messages
if (err) {
qDebug() << "[CloneJob]" << git_error_last()->message;
}
}
if (repo) {
git_repository_free(repo);
}
return ret == 0;
}

105
plugins/Git/jobs/clonejob.h Normal file
View File

@ -0,0 +1,105 @@
#ifndef CLONEJOB_H
#define CLONEJOB_H
#include <QDir>
extern "C" {
#include <git2.h>
}
#include "gitjob.h"
/**
* @class CloneJob
* @brief A class to handle cloning Git repositories in a separate thread.
*
*/
class CloneJob : public GitJob
{
Q_OBJECT
/**
* @brief The main function that performs the cloning operation.
*
* Handles the process of cloning a repository from the specified URL
* to the target path.
*/
void run() override;
signals:
/**
* @brief Signal emitted when the cloning operation is complete.
*
* This signal is emitted once the cloning operation finishes.
*
* @param err A boolean indicating whether an error occurred during cloning.
* `true` if an error occurred, `false` if the clone was successful.
*/
void resultReady(const bool err);
private:
QString m_url; ///< The URL of the Git repository to clone.
QString m_path; ///< The destination path for the cloned repository.
/**
* @brief Prepares the temporary directory for cloning.
*
* This method sets up the required directory structure for cloning a repository.
*
* @return A `QDir` object representing the prepared temporary directory.
*/
static QDir cloneSetup();
/**
* @brief Moves the cloned repository to the specified destination.
*
* After the repository has been cloned into a temporary directory, this method
* moves it to the final destination specified by the user.
*
* @param path The destination path to move the repository to.
* @param tmp_dir The temporary directory where the repository was cloned.
* @return `true` if the move was successful, `false` otherwise.
*/
static bool moveToDestination(QDir tmp_dir, QString path);
/**
* @brief Tears down the temporary directory after cloning.
*
* This method is called to clean up the temporary directory after the repository
* has been cloned and moved to the final destination. It removes the temporary
* directory and all its contents.
*
* @param tmp_dir The temporary directory to tear down.
* @return `true` if the teardown was successful, `false` otherwise.
*/
static bool cloneCleanUp(QDir tmp_dir);
/**
* @brief Clones a repository from a specified URL.
*
* This method handles the actual cloning process by setting up a temporary
* directory, cloning the repository into it, and then moving it to the final
* destination. It uses the provided credentials to authenticate the cloning
* operation.
*
* @param url The URL of the Git repository to clone.
* @param path The destination path for the cloned repository.
* @param cred The credentials to use for the cloning operation.
* @param cb The callback function for acquiring credentials during cloning.
* @return `true` if the cloning process was successful, `false` otherwise.
*/
static bool clone(QString url, QString path, cred_type cred, git_cred_acquire_cb cb);
public:
/**
* @brief Constructor for the CloneJob class.
*
* Initializes the CloneJob with the specified repository URL, destination path,
* and credentials.
*
* @param url The URL of the Git repository to clone.
* @param path The destination path where the repository will be cloned.
* @param cred The credentials to be used for the cloning process.
*/
CloneJob(QString url, QString path, cred_type cred);
};
#endif // CLONEJOB_H

View File

@ -0,0 +1,74 @@
#include <QDebug>
#include "gitjob.h"
#include "../utils.h"
extern "C" {
#include <git2.h>
}
GitJob::GitJob(cred_type cred) :
m_cred(cred)
{
git_libgit2_init();
}
GitJob::~GitJob()
{
git_libgit2_shutdown();
}
int GitJob::credentialsCB(git_cred **out, const char *url, const char *username_from_url,
unsigned int allowed_types, void *payload)
{
UNUSED(url);
PayloadCB *p = (PayloadCB *)payload;
if (!username_from_url) { // we required here that the username must be provided directly in url (maybe improve later on)
qWarning() << "[GitJob] credentials_cb : no username provided";
return (int) GIT_EUSER;
}
if (p->called) {
qWarning() << "[GitJob] credentials_cb : cb already called, probably invalid creds";
return (int) GIT_EUSER;
}
p->called = true;
auto v = overload {
[](const HTTP & x)
{
UNUSED(x);
qDebug() << "[GitJob] credentialsCB : HTTP ";
qWarning() << "[GitJob] credentialsCB : callback should never be call for HTTP";
return (int) GIT_EUSER;
},
[allowed_types, &out, &username_from_url](const HTTPUserPass & x)
{
qDebug() << "[GitJob] credentialsCB : HTTPUserPass ";
if (!(allowed_types & GIT_CREDTYPE_USERPASS_PLAINTEXT)) {
qWarning() << "[GitJob] credentials_cb : allowed_types is invalid for HTTPUserPass creds";
return (int) GIT_EUSER;
}
return git_cred_userpass_plaintext_new(out, username_from_url, x.pass.toLocal8Bit().constData());
},
[allowed_types, &out, &username_from_url](const SSHKey & x)
{
qDebug() << "[GitJob] credentialsCB : SSHKey ";
if (!(allowed_types & GIT_CREDTYPE_SSH_KEY)) {
qWarning() << "[GitJob] credentials_cb : allowed_types is invalid for SSHKey creds";
return (int) GIT_EUSER;
}
qDebug() << "[GitJob] username_from_url :" << username_from_url;
qDebug() << "[GitJob] pub_key :" << x.pub_key.toLocal8Bit().constData();
qDebug() << "[GitJob] priv_key :" << x.priv_key.toLocal8Bit().constData();
qDebug() << "[GitJob] passphrase :" << x.passphrase.toLocal8Bit().constData();
return git_cred_ssh_key_new(out, username_from_url, x.pub_key.toLocal8Bit().constData(),
x.priv_key.toLocal8Bit().constData(), x.passphrase.toLocal8Bit().constData());
}
};
auto ret = std::visit(v, p->creds);
return ret;
}

95
plugins/Git/jobs/gitjob.h Normal file
View File

@ -0,0 +1,95 @@
#ifndef GITJOB_H
#define GITJOB_H
#include <QDir>
#include <QThread>
extern "C" {
#include <git2.h>
}
#include <variant>
// Forward declarations for the different credential types.
struct HTTP {};
struct HTTPUserPass {
QString pass; ///< Password for HTTP user authentication.
};
struct SSHKey {
QString pub_key; ///< public key path.
QString priv_key; ///< private key path.
QString passphrase; ///< key passphrase.
};
/**
* @brief Variant type to represent various types of credentials.
*
* This type is used to store one of the following credential types:
* - HTTP
* - HTTPUserPass
* - SSHKey
*/
typedef std::variant<HTTP, HTTPUserPass, SSHKey> cred_type;
struct PayloadCB
{
bool called;
cred_type creds;
PayloadCB(bool ca, cred_type cr): called(ca), creds(cr) {}
};
/**
* @class GitJob
* @brief A class that manages Git-related tasks using libgit2.
*
* The GitJob class is used abstraction class to perform Git operations, such as cloning repositories,
* in a separate thread using libgit2 for interacting with Git repositories.
*/
class GitJob : public QThread
{
Q_OBJECT
protected:
cred_type m_cred; ///< The credentials used for Git operations.
/**
* @brief Callback function for handling Git credentials during cloning.
*
* This function is called by libgit2 to handle credentials for cloning a repository.
* The callback is invoked when Git needs to authenticate with a remote repository.
*
* @param out Pointer to the credentials object that will be populated by the callback.
* @param url The URL of the repository being cloned.
* @param username_from_url The username extracted from the URL (if applicable).
* @param allowed_types A bitmask of the allowed types of credentials that can be used.
* @param payload User-defined data passed to the callback. This is typically the instance
* of the class providing the callback, or other user-defined data.
*
* @return A status code indicating success (0) or failure (non-zero).
* @see git_cred
* @see git_cred_type
*/
static int credentialsCB(git_cred **out, const char *url, const char *username_from_url,
unsigned int allowed_types, void *payload);
public:
/**
* @brief Constructor for the GitJob class.
*
* Initializes the GitJob instance with the given credentials.
*
* @param cred The credentials to be used for the Git operation. This can be one of
* the following types: HTTP, HTTPUserPass, SSHPass, or SSHKey.
*/
GitJob(cred_type cred);
/**
* @brief Destructor for the GitJob class.
*
* Cleans up any resources used by the GitJob.
*/
~GitJob();
};
#endif // GITJOB_H

View File

@ -1,89 +0,0 @@
#include <QUrl>
#include <QDebug>
#include <type_traits>
extern "C" {
#include <git2.h>
}
#include "libgit.h"
#include "utils.h"
LibGit::LibGit()
{
git_libgit2_init();
}
LibGit::~LibGit()
{
git_libgit2_shutdown();
}
void LibGit::set_mode(mode_type type)
{
this->mode = type;
}
int LibGit::credentials_cb(git_cred **out, const char *url, const char *username_from_url,
unsigned int allowed_types, void *payload)
{
// TODO : More precise Error Handling for UI
auto instance = LibGit::instance();
auto v = overload {
[](const Unset & x)
{
qDebug() << "credentials_cb : Unset ";
qWarning() << "credentials_cb : callback should never be call for Unset ";
return (int) GIT_EUSER;
},
[](const HTTP & x)
{
qDebug() << "credentials_cb : HTTP ";
qWarning() << "credentials_cb : callback should never be call for HTTP ";
return (int) GIT_EUSER;
},
[&out, &username_from_url](const HTTPAuth & x)
{
qDebug() << "credentials_cb : HTTPAuth ";
if (!username_from_url) {
qWarning() << "credentials_cb : no username provided ";
return (int) GIT_EUSER;
}
return git_cred_userpass_plaintext_new(out, username_from_url, x.pass.toLocal8Bit().constData());
},
[&](const SSHAuth & x)
{
qWarning() << "credentials_cb : SSHAuth to be implemented ";
return (int) GIT_EUSER;
}, // TODO
[&](const SSHKey & x)
{
qWarning() << "credentials_cb : SSHKey to be implemented ";
return (int) GIT_EUSER;
} // TODO
};
return std::visit(v, instance->mode);
}
bool LibGit::clone(QString url, QString path)
{
git_repository *repo = NULL;
git_clone_options opts = GIT_CLONE_OPTIONS_INIT;
opts.fetch_opts.callbacks.credentials = *credentials_cb;
int ret = git_clone(&repo, url.toLocal8Bit().data(), path.toLocal8Bit().data(), &opts);
if (ret != 0) {
qDebug() << git_error_last()->message;
}
if (repo) {
git_repository_free(repo);
}
return ret == 0; // TODO Clean error handling to return specifics errors for the ui
return ret;
}

View File

@ -1,46 +0,0 @@
#ifndef LIBGIT_H
#define LIBGIT_H
#include <QObject>
#include <QUrl>
#include <git2/clone.h>
extern "C" {
#include <git2/transport.h>
}
#include <memory>
#include <variant>
struct Unset { };
struct HTTP { };
struct HTTPAuth {
QString pass;
};
struct SSHAuth { };
struct SSHKey { };
typedef std::variant<Unset, HTTP, HTTPAuth, SSHAuth, SSHKey> mode_type;
class LibGit
{
private:
LibGit();
mode_type mode;
static int credentials_cb(git_cred **out, const char *url, const char *username_from_url,
unsigned int allowed_types, void *payload);
public:
~LibGit();
static std::shared_ptr<LibGit> instance()
{
static std::shared_ptr<LibGit> s{new LibGit};
return s;
}
LibGit(LibGit const &) = delete;
void operator=(LibGit const &) = delete;
bool clone(QString url, QString path);
void set_mode(mode_type type);
};
#endif

View File

@ -9,5 +9,4 @@ void GitPlugin::registerTypes(const char *uri)
{ {
//@uri Git //@uri Git
qmlRegisterSingletonType<Git>(uri, 1, 0, "Git", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Git; }); qmlRegisterSingletonType<Git>(uri, 1, 0, "Git", [](QQmlEngine *, QJSEngine *) -> QObject * { return new Git; });
} }

View File

@ -1,10 +1,20 @@
#ifndef UTILS_H #ifndef UTILS_H
#define UTILS_H #define UTILS_H
#define UNUSED(x) (void)(x)
/**
* @brief A utility structure for enabling function overloading with template-based classes.
* see : https://stackoverflow.com/a/64018031
*/
template<class... Ts> template<class... Ts>
struct overload : Ts... { struct overload : Ts... {
using Ts::operator()...; using Ts::operator()...;
}; };
/**
* @brief Deduction guide for the `overload` template.
* see : https://stackoverflow.com/a/64018031
*/
template<class... Ts> template<class... Ts>
overload(Ts...) -> overload<Ts...>; overload(Ts...) -> overload<Ts...>;

View File

@ -5,9 +5,15 @@ set(
SRC SRC
plugin.cpp plugin.cpp
pass.cpp pass.cpp
gpg.cpp passkeyringmodel.h
passkeymodel.h
passphraseprovider.h passphraseprovider.h
error.h
jobs/decryptjob.cpp
jobs/deletekeyjob.cpp
jobs/getkeysjob.cpp
jobs/importkeyjob.cpp
jobs/rmjob.cpp
jobs/rnpjob.cpp
) )
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
@ -27,24 +33,23 @@ set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN})
qt5_use_modules(${PLUGIN} Qml Quick DBus) qt5_use_modules(${PLUGIN} Qml Quick DBus)
add_library(gpgerror SHARED IMPORTED) set(RNP_INSTALL_DIR "${CMAKE_SOURCE_DIR}/build/${ARCH_TRIPLET}/rnp/install")
set_property(TARGET gpgerror PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpg-error.so.0.28.0") set(BOTAN_INSTALL_DIR "${CMAKE_SOURCE_DIR}/build/${ARCH_TRIPLET}/botan/install")
add_library(libassuan SHARED IMPORTED) find_package(JSON-C 0.11)
set_property(TARGET libassuan PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libassuan.so")
add_library(libgpgme SHARED IMPORTED) INCLUDE_DIRECTORIES(${RNP_INSTALL_DIR}/include)
set_property(TARGET libgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgme.so")
add_library(libgpgmepp SHARED IMPORTED) add_library(rnp STATIC IMPORTED)
set_property(TARGET libgpgmepp PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgmepp.so") set_property(TARGET rnp PROPERTY IMPORTED_LOCATION "${RNP_INSTALL_DIR}/lib/librnp.a")
add_library(libqgpgme SHARED IMPORTED) add_library(sexpp STATIC IMPORTED)
set_property(TARGET libqgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libqgpgme.so") set_property(TARGET sexpp PROPERTY IMPORTED_LOCATION "${RNP_INSTALL_DIR}/lib/libsexpp.a")
add_library(botan STATIC IMPORTED)
set_property(TARGET botan PROPERTY IMPORTED_LOCATION "${BOTAN_INSTALL_DIR}/lib/libbotan-2.a")
target_link_libraries(${PLUGIN} gpgerror libassuan libgpgme libgpgmepp libqgpgme) target_link_libraries(${PLUGIN} rnp sexpp botan JSON-C::JSON-C)
set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}")
install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/) install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/)

71
plugins/Pass/error.h Normal file
View File

@ -0,0 +1,71 @@
// error.h
#ifndef ERROR_H
#define ERROR_H
extern "C" {
#include "rnp/rnp_err.h"
}
enum class ErrorCodeShow {
Success= 0,
UnexceptedError,
BadPassphrase,
NoKeyFound,
DecryptFailed
};
int rnpErrorToErrorCodeShow(int rnpErrorCode) {
switch (rnpErrorCode) {
case RNP_SUCCESS:
return static_cast<int>(ErrorCodeShow::Success);
case RNP_ERROR_BAD_PASSWORD:
return static_cast<int>(ErrorCodeShow::BadPassphrase);
case RNP_ERROR_KEY_NOT_FOUND:
case RNP_ERROR_NO_SUITABLE_KEY:
return static_cast<int>(ErrorCodeShow::NoKeyFound);
case RNP_ERROR_DECRYPT_FAILED:
return static_cast<int>(ErrorCodeShow::DecryptFailed);
default:
return static_cast<int>(ErrorCodeShow::UnexceptedError);
}
}
enum class ErrorCodeImportKeyFile {
Success= 0,
UnexceptedError,
BadFormat,
};
int rnpErrorToErrorCodeImportKeyFile(int rnpErrorCode) {
switch (rnpErrorCode) {
case RNP_SUCCESS:
return static_cast<int>(ErrorCodeShow::Success);
case RNP_ERROR_BAD_FORMAT:
return static_cast<int>(ErrorCodeImportKeyFile::BadFormat);
default:
return static_cast<int>(ErrorCodeImportKeyFile::UnexceptedError);
}
}
enum class ErrorCodeUnexvepted {
Success= 0,
UnexceptedError,
};
int rnpErrorToErrorCodeGeneric(int rnpErrorCode) {
switch (rnpErrorCode) {
case RNP_SUCCESS:
return static_cast<int>(ErrorCodeShow::Success);
default:
return static_cast<int>(ErrorCodeImportKeyFile::UnexceptedError);
}
}
enum class ErrorCode
{
Success= 0,
Error,
};
#endif // ERROR_H

View File

@ -1,295 +0,0 @@
#include <memory>
#include <QDebug>
#include <QFile>
#include <QDir>
#include <QtCore/QStandardPaths>
#include <gpgme.h>
#include <gpgme++/data.h>
#include <gpgme++/global.h>
#include <gpgme++/context.h>
#include <gpgme++/engineinfo.h>
#include <gpgme++/keylistresult.h>
#include <gpgme++/importresult.h>
#include <gpgme++/encryptionresult.h>
#include <gpgme++/decryptionresult.h>
#include <qgpgme/importjob.h>
#include <qgpgme/deletejob.h>
#include <qgpgme/decryptjob.h>
#include <qgpgme/encryptjob.h>
#include <qgpgme/protocol.h>
#include <qgpgme/keylistjob.h>
#include <qgpgme/changeownertrustjob.h>
#include "gpg.h"
#include "passphraseprovider.h"
#include "qprocess.h"
using namespace GpgME;
using namespace QGpgME;
Gpg::Gpg()
{
m_window = nullptr;
Gpg::initGpgConfig();
auto error = checkEngine(OpenPGP);
if (error) {
qDebug() << "Code Error : " << error.code();
qDebug() << "Error str : " << error.asString();
qFatal("GNUPG Engine check Fail");
}
qDebug() << "GNUPG Engine Version is :" << engineInfo(OpenPGP).version();
qDebug() << "GNUPG Executable is :" << engineInfo(OpenPGP).fileName();
qDebug() << "GNUPG Home is :" << engineInfo(OpenPGP).homeDirectory();
}
QString Gpg::initGpgHome()
{
QString path = QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation).append("/.gpghome");
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
return path;
}
QString Gpg::findCommandPath(const QString &command)
{
// Retrieve the PATH environment variable
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
QString pathEnv = env.value("PATH");
// Split the PATH by colon
QStringList pathDirs = pathEnv.split(":", QString::SkipEmptyParts);
// Check each directory in the PATH
foreach (const QString &dir, pathDirs) {
QFileInfo fileInfo(QDir(dir).filePath(command));
// If the file exists and is executable, return the path
if (fileInfo.exists() && fileInfo.isExecutable()) {
return fileInfo.absoluteFilePath();
}
}
return QString::null;
}
QString Gpg::initGpgExec()
{
QString path = findCommandPath("gpg");
if (path.isNull()) {
qFatal("No valid gpg exec found !");
}
return path;
}
void Gpg::initGpgConfig()
{
initializeLibrary();
gpgme_set_global_flag("disable-gpgconf", "1");
QString home = initGpgHome();
qDebug() << "Gpg home is " << home;
QString exec = initGpgExec();
qDebug() << "Gpg exec is " << exec;
QFile agentConf(home + QStringLiteral("/gpg-agent.conf"));
agentConf.remove();
agentConf.open(QIODevice::WriteOnly);
agentConf.write("allow-loopback-pinentry\n");
agentConf.close();
auto err = gpgme_set_engine_info (
GPGME_PROTOCOL_OpenPGP,
exec.toLocal8Bit().data(),
home.toLocal8Bit().data()
);
if (err != GPG_ERR_NO_ERROR) {
qDebug() << "Error code : " << err;
qDebug() << "Error str : " << gpg_strerror(err);
qFatal("GPGME set engine info failed !");
}
}
QPair<Error, QString> Gpg::decrypt(QByteArray cipherText)
{
auto job = openpgp()->decryptJob();
auto ctx = DecryptJob::context(job);
auto provider = new UTPassphraseProvider;
ctx->setPassphraseProvider(provider);
ctx->setPinentryMode(Context::PinentryLoopback);
QByteArray plain_text;
auto decResult = job->exec(cipherText, plain_text);
delete job;
if (decResult.error()) {
qWarning() << "something gone wrong on decrypt";
qDebug() << "Code Error : " << decResult.error().code();
qDebug() << "Error str : " << decResult.error().asString();
}
return QPair<Error, QString>(decResult.error(), QString::fromUtf8(plain_text));
}
QPair<Error, QString> Gpg::decryptFromFile(QString path)
{
qDebug() << "Decrypt from " << path;
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Can't open the File";
return QPair<Error, QString>(Error(), QString());;
}
QByteArray cipherText = file.readAll();
file.close();
return decrypt(cipherText);
}
QPair<Error, QByteArray> Gpg::encrypt(QString str, QString uid, bool ascii_armor, bool text_mode)
{
qDebug() << "Encrypt to QByteArray";
auto keys = getKeys(uid);
if (keys.first) {
return QPair<Error, QByteArray>(keys.first, QByteArray());
}
auto job = std::unique_ptr<EncryptJob>(openpgp()->encryptJob(ascii_armor, text_mode));
QByteArray cipherText;
auto result = job->exec(keys.second, str.toUtf8(), Context::AlwaysTrust, cipherText);
qDebug() << "Encrypted to QByteArray";
return QPair<Error, QByteArray>(result.error(), cipherText);
}
Error Gpg::encryptToFile(QString str, QString path, QString uid, bool ascii_armor,
bool text_mode)
{
qDebug() << "Encrypting to file " << path;
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Can't open the file to write it" ;
return Error();
}
auto encrypt_ret = encrypt(str, uid, ascii_armor, text_mode);
if (encrypt_ret.first) {
file.write(encrypt_ret.second);
}
qDebug() << "Encrypting to file " << path;
return encrypt_ret.first;
}
QPair<Error, std::vector< GpgME::Key >> Gpg::getAllKeys ( bool remote, const bool include_sigs,
bool validate )
{
return getKeys(QString(""), remote, include_sigs, validate);
}
QPair<Error, std::vector<Key >> Gpg::getKeys(QString pattern_uid, bool remote, bool include_sigs,
bool validate)
{
qDebug() << "Getting the keys " << pattern_uid;
auto job = std::unique_ptr<KeyListJob>(openpgp()->keyListJob(remote, include_sigs, validate));
std::vector<Key> keys;
auto result = job->exec(QStringList() << pattern_uid, false, keys);
qDebug() << "Got the keys " << pattern_uid;
return QPair<Error, std::vector< Key >> (result.error(), keys);
}
QPair<Error, Key> Gpg::getKey(QString uid, bool remote, bool include_sigs, bool validate)
{
qDebug() << "Getting the key " << uid;
auto keys = getKeys(uid, remote, include_sigs, validate);
if (keys.first or keys.second.size() != 1) {
qWarning() << "Bad id";
return QPair<Error, Key>(keys.first, Key::null);
}
qDebug() << "Got the key " << uid;
return QPair<Error, Key>(keys.first, keys.second.front());
}
Error Gpg::importKeysFromFile(QString path)
{
qDebug() << "Importing the key file" << path;
qDebug() << "Decrypt from " << path;
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Can't open the File";
return Error();
}
auto job = openpgp()->importJob();
auto ctx = ImportJob::context(job);
auto provider = new UTPassphraseProvider;
ctx->setPassphraseProvider(provider);
ctx->setPinentryMode(Context::PinentryLoopback);
auto result = job->exec(file.readAll());
qDebug() << "numImported" << result.numImported();
qDebug() << "numSecretKeysImported" << result.numSecretKeysImported();
qDebug() << "numSecretKeysConsidered" << result.numSecretKeysConsidered();
qDebug() << "numSecretKeysUnchanged" << result.numSecretKeysUnchanged();
qDebug() << "numUnchanged" << result.numUnchanged();
file.close();
delete job;
delete provider;
if (result.error()) {
qWarning() << "Import go wrong";
qDebug() << "Code Error : " << result.error().code();
qDebug() << "Error str : " << result.error().asString();
}
qDebug() << "Imported the key file" << path;
return result.error();
}
Error Gpg::deleteKeyId(QString uid)
{
qDebug() << "Deleting key id " << uid;
auto key = getKey(uid);
if (key.first) {
return key.first;
}
auto ctx = std::unique_ptr<Context>(Context::createForProtocol(OpenPGP));
auto err = ctx->deleteKey(key.second, true);
if (err) {
qWarning() << "Delete go wrong";
qDebug() << "Code Error : " << err.code();
qDebug() << "Error str : " << err.asString();
return err;
}
qDebug() << "Deleted key id" << uid;
return err;
}

View File

@ -1,61 +0,0 @@
#ifndef GPG_H
#define GPG_H
#include <memory>
#include <QQuickWindow>
#include <gpgme++/context.h>
#include <qgpgme/changeownertrustjob.h>
using namespace GpgME;
class Gpg
{
private:
Gpg();
QObject *m_window;
QString findCommandPath(const QString &command);
QString initGpgHome();
QString initGpgExec();
void initGpgConfig();
public:
static std::shared_ptr<Gpg> instance()
{
static std::shared_ptr<Gpg> s{new Gpg};
return s;
}
Gpg(Gpg const &) = delete;
void operator=(Gpg const &) = delete;
void setWindow(QObject *window)
{
m_window = window;
};
QObject *getWindow()
{
return m_window;
};
QPair<Error, std::vector<Key >> getAllKeys(bool remote = false, bool include_sigs = {}, bool
validate = false);
QPair<Error, std::vector<Key >> getKeys( QString pattern_uid, bool remote = false,
bool include_sigs = false,
bool validate = false);
QPair<Error, Key> getKey( QString uid, bool remote = false, bool include_sigs = false,
bool validate = false);
QPair<Error, QString> decrypt( QByteArray cipherText);
QPair<Error, QString> decryptFromFile( QString path);
QPair<Error, QByteArray> encrypt( QString str, QString uid, bool ascii_armor = true,
bool text_mode = true);
Error encryptToFile( QString str, QString path, QString uid, bool ascii_armor = true,
bool text_mode = true);
Error importKeysFromFile( QString path);
Error deleteKeyId( QString uid);
};
#endif

View File

@ -0,0 +1,47 @@
#include "decryptjob.h"
#include "qdebug.h"
extern "C" {
#include <rnp/rnp.h>
#include <rnp/rnp_err.h>
}
DecryptJob::DecryptJob(QDir rnp_homedir, QString path):
RnpJob(rnp_homedir),
m_encrypted_file_path(path)
{
this->setObjectName("DecryptJob");
}
void DecryptJob::run()
{
qDebug() << "[DecryptJob] Starting";
this->loadFullKeyring(NULL);
rnp_input_t input = NULL;
rnp_output_t output = NULL;
uint8_t *buf = NULL;
size_t buf_len = 0;
QString data = QString::Null();
auto ret = rnp_input_from_path(&input, this->m_encrypted_file_path.toLocal8Bit().data());
if (ret == RNP_SUCCESS) {
ret = rnp_output_to_memory(&output, 0);
}
if (ret == RNP_SUCCESS) {
ret = rnp_decrypt(this->m_ffi, input, output);
}
if (ret == RNP_SUCCESS) {
ret = rnp_output_memory_get_buf(output, &buf, &buf_len, false);
}
if (ret == RNP_SUCCESS) {
data = QString::fromUtf8((char*)buf, buf_len);
}
rnp_input_destroy(input);
rnp_output_destroy(output);
terminateOnError(ret);
emit resultSuccess(this->m_encrypted_file_path, data);
qDebug() << "[DecryptJob] Finished Successfully ";
}

View File

@ -0,0 +1,54 @@
#ifndef DECRYPTJOB_H
#define DECRYPTJOB_H
#include "rnpjob.h"
#include <QThread>
#include <QDir>
/**
* @class DecryptJob
* @brief A job to handle the decryption of a file in a separate thread.
*
*/
class DecryptJob : public RnpJob
{
Q_OBJECT
/**
* @brief Executes the decryption operation.
*
* This method performs the actual decryption of the encrypted file specified during
* object construction.
*/
void run() override;
signals:
/**
* @brief Emitted when the decryption operation is complete.
*
* This signal is emitted once the decryption operation finishes, providing the results.
* It indicates whether the decryption was successful and provides the clear-text output
* if the decryption was successful.
*
* @param encrypted_file_path The path to the encrypted file that was decrypted.
* @param clear_txt The decrypted content in clear-text. If an error occurs, this may be empty.
*/
void resultSuccess(QString encrypted_file_path, QString clear_txt);
private:
QString m_encrypted_file_path; /**< The path to the encrypted file that is to be decrypted. */
public:
/**
* @brief Constructs a DecryptJob object with the specified encrypted file.
*
* This constructor initializes the DecryptJob with the encrypted file path. The decryption
* operation will be executed in a background thread when the job is started.
*
* @param rnp_homedir The directory containing the keyrings.
* @param path The path to the encrypted file that needs to be decrypted.
*/
DecryptJob(QDir rnp_homedir, QString path);
};
#endif // DECRYPTJOB_H

View File

@ -0,0 +1,42 @@
#include <QDebug>
#include <QString>
#include <QJsonDocument>
#include "deletekeyjob.h"
extern "C" {
#include <rnp/rnp.h>
#include <rnp/rnp_err.h>
}
DeleteKeyJob::DeleteKeyJob(QDir rnp_homedir, QString fingerprint):
RnpJob(rnp_homedir),
m_fingerprint(fingerprint)
{
this->setObjectName("ImportKeyJob");
}
void DeleteKeyJob::run()
{
qDebug() << "[DeleteKeyJob] Starting";
// Loading keyring
this->loadFullKeyring(NULL);
// Delete key
rnp_key_handle_t key = NULL;
auto ret = rnp_locate_key(this->m_ffi, "fingerprint", this->m_fingerprint.toLocal8Bit().data(), &key);
if (ret == RNP_SUCCESS) {
ret = rnp_key_remove(key, RNP_KEY_REMOVE_PUBLIC | RNP_KEY_REMOVE_SECRET | RNP_KEY_REMOVE_SUBKEYS);
};
rnp_key_handle_destroy(key);
// Save resulting keyring
this->saveFullKeyring();
// Emit result
terminateOnError(ret);
emit resultSuccess();
qDebug() << "[DeleteKeyJob] Finished Successfully ";
}

View File

@ -0,0 +1,46 @@
#ifndef DELETEKEYJOB_H
#define DELETEKEYJOB_H
#include "rnpjob.h"
/**
* @class DeleteKeyJob
* @brief A job to handle the deletion of a key in a separate thread.
*
*/
class DeleteKeyJob : public RnpJob
{
Q_OBJECT
/**
* @brief Executes the key deletion operation.
*
* This function performs the actual process of deleting the GPG key from the keyring.
*/
void run() override;
signals:
/**
* @brief Emitted when the key deletion operation is successful.
*
* This signal is emitted when the key is successfully deleted from the keyring..
*/
void resultSuccess();
private:
QString m_fingerprint; /**< The fingerprint of the key to delete. */
public:
/**
* @brief Constructs a DeleteKeyJob object with the specified fingerprint and keyring directory.
*
* This constructor initializes the DeleteKeyJob instance with the directory containing
* the keyrings and the fingerprint of the GPG key to delete.
*
* @param rnp_homedir The directory containing the keyrings where the key will be deleted.
* @param fingerprint The fingerprint of the key to delete.
*/
DeleteKeyJob(QDir rnp_homedir, QString fingerprint);
};
#endif // DELETEKEYJOB_H

View File

@ -0,0 +1,45 @@
#include <QDebug>
#include "getkeysjob.h"
#include <QJsonDocument>
#include <QJsonObject>
extern "C" {
#include <rnp/rnp.h>
#include <rnp/rnp_err.h>
}
GetKeysJob::GetKeysJob(QDir rnp_homedir):
RnpJob(rnp_homedir)
{
this->setObjectName("GetKeysJob");
}
QJsonDocument GetKeysJob::fingerprint_map_key_info(const QString fingerprint)
{
rnp_key_handle_t handle;
rnp_locate_key(this->m_ffi, "fingerprint", fingerprint.toLocal8Bit().data(), &handle);
char *result;
rnp_key_to_json( handle, 0, &result);
return QJsonDocument::fromJson(result);
}
void GetKeysJob::run()
{
qDebug() << "[GetKeysJob] Starting";
// Loading keyring
QSet<QString> fingerprints = QSet<QString>();
this->loadFullKeyring(&fingerprints);
//Get infos keys
auto key_infos = QList<QJsonDocument>();
QList<QJsonDocument>::iterator i;
for (auto i = fingerprints.begin(), end = fingerprints.end(); i != end; ++i) {
key_infos.append(this->fingerprint_map_key_info(*i));
}
// Emit result
emit resultSuccess(key_infos);
qDebug() << "[GetKeysJob] Finished Successfully ";
}

View File

@ -0,0 +1,58 @@
#ifndef GETKEYSJOB_H
#define GETKEYSJOB_H
#include <QJsonDocument>
#include "rnpjob.h"
/**
* @class GetKeysJob
* @brief A job to retrieve all GPG keys from keyrings in a separate thread.
*
*/
class GetKeysJob : public RnpJob
{
Q_OBJECT
/**
* @brief Executes the process of fetching all GPG keys.
*
* This function performs the task of retrieving all keys from the keyrings.
*/
void run() override;
signals:
/**
* @brief Emitted when the key retrieval operation completes successfully.
*
* This signal is emitted when the keys are successfully fetched. It passes a list of
* JSON documents representing the retrieved keys.
*
* @param result A list of QJsonDocument objects containing the key information.
*/
void resultSuccess(const QList<QJsonDocument> result);
private:
/**
* @brief Retrieves key information for a specific key fingerprint.
*
* This helper function fetches the key data corresponding to the given fingerprint.
* The returned information is packaged in a JSON document.
*
* @param fingerprint The fingerprint of the key to fetch information for.
* @return A QJsonDocument containing the key's information.
*/
QJsonDocument fingerprint_map_key_info(const QString fingerprint);
public:
/**
* @brief Constructs a GetKeysJob object with the specified keyring directory.
*
* This constructor initializes the job with the directory containing the keyrings to
* search for GPG keys.
*
* @param rnp_homedir The directory that contains the keyrings.
*/
GetKeysJob(QDir rnp_homedir);
};
#endif // GETKEYSJOB_H

View File

@ -0,0 +1,49 @@
#include <QDebug>
#include <QString>
#include <QJsonDocument>
#include "importkeyjob.h"
extern "C" {
#include <rnp/rnp.h>
#include <rnp/rnp_err.h>
}
ImportKeyJob::ImportKeyJob(QDir rnp_homedir, QString key_file_path):
RnpJob(rnp_homedir),
m_key_file_path(key_file_path)
{
this->setObjectName("ImportKeyJob");
}
void ImportKeyJob::run()
{
qDebug() << "[ImportKeyJob] Starting";
// Loading keyring
this->loadFullKeyring(NULL);
// Import new key
rnp_input_t input = NULL;
auto ret = rnp_input_from_path(&input, this->m_key_file_path.toLocal8Bit().constData());
if (ret == RNP_SUCCESS) {
char *r = NULL;
ret = rnp_import_keys(this->m_ffi,
input,
RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS,
&r);
qDebug() << "[ImportKeyJob]" << QJsonDocument::fromJson(r);
rnp_buffer_destroy(r);
}
rnp_input_destroy(input);
terminateOnError(ret);
// Save resulting keyring
this->saveFullKeyring();
// Emit result
emit resultSuccess();
qDebug() << "[ImportKeyJob] Finished Successfully ";
}

View File

@ -0,0 +1,47 @@
#ifndef IMPORTKEYJOB_H
#define IMPORTKEYJOB_H
#include "rnpjob.h"
/**
* @class ImportKeyJob
* @brief A job to handle the import of a key file in a separate thread.
*
*/
class ImportKeyJob : public RnpJob
{
Q_OBJECT
/**
* @brief Executes the key import operation.
*
* This function handles the actual process of importing the GPG key file into the
* keyring.
*/
void run() override;
signals:
/**
* @brief Emitted when the key import operation is successful.
*
* This signal is emitted when the key file is successfully imported into the keyring.
*/
void resultSuccess();
private:
QString m_key_file_path; /**< The path of the key file to import. */
public:
/**
* @brief Constructs an ImportKeyJob object with the specified key file and keyring directory.
*
* This constructor initializes the ImportKeyJob instance with the directory containing
* the keyrings and the file path of the GPG key to import.
*
* @param rnp_homedir The directory containing the keyrings.
* @param path The path to the key file to import.
*/
ImportKeyJob(QDir rnp_homedir, QString path);
};
#endif // IMPORTKEYJOB_H

View File

@ -0,0 +1,24 @@
#include "rmjob.h"
RmJob::RmJob(QString path):
m_path(path)
{
this->setObjectName("RmJob");
}
void RmJob::run()
{
auto info = QFileInfo(this->m_path);
if (info.isFile()) {
auto file = QFile(this->m_path);
file.remove();
emit resultReady(false);
} else if (info.isDir()) {
auto dir = QDir(this->m_path);
dir.removeRecursively();
emit resultReady(false);
} else {
emit resultReady(true);
}
}

50
plugins/Pass/jobs/rmjob.h Normal file
View File

@ -0,0 +1,50 @@
#ifndef RMJOB_H
#define RMJOB_H
#include <QThread>
#include <QDir>
/**
* @class RmJob
* @brief A job to handle the recursive removal of a path in a separate thread.
*
*/
class RmJob : public QThread
{
Q_OBJECT
/**
* @brief Executes the recursive remove operation.
*
* This method performs the recursive removal of the specified target path.
*/
void run() override;
signals:
/**
* @brief Emitted when the remove operation completes.
*
* This signal is emitted once the removal process is complete, indicating
* whether the operation succeeded or failed.
*
* @param err A boolean indicating whether an error occurred during the removal.
* `true` if an error occurred, `false` if the operation was successful.
*/
void resultReady(const bool err);
private:
QString m_path; /**< The path to be removed. */
public:
/**
* @brief Constructs an RmJob object with the specified path.
*
* This constructor initializes the job with the path of the directory or file to be
* removed. The job will be executed in a separate thread when started.
*
* @param path The path to the file or directory that should be removed.
*/
RmJob(QString path);
};
#endif // RMJOB_H

View File

@ -0,0 +1,134 @@
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#include "qjsonarray.h"
#include "rnpjob.h"
extern "C" {
#include <rnp/rnp.h>
#include <rnp/rnp_err.h>
}
RnpJob::RnpJob(QDir rnp_homedir):
m_rnp_homedir(rnp_homedir)
{
qRegisterMetaType<rnp_result_t>("rnp_result_t");
qRegisterMetaType<QList<QJsonDocument >> ("QList<QJsonDocument>");
qRegisterMetaType<QString *>("QString*");
auto ret = rnp_ffi_create(&this->m_ffi,
RNP_KEYSTORE_GPG,
RNP_KEYSTORE_GPG);
if (ret != RNP_SUCCESS) {
qDebug() << "[RnpJob] Err : " << ret;
qFatal("Error on rnp ffi init!");
}
}
RnpJob::~RnpJob()
{
auto ret = rnp_ffi_destroy(this->m_ffi);
if (ret != RNP_SUCCESS) {
qDebug() << "[RnpJob] Err : " << ret;
qFatal("Something go wrong on rnp ffi detroy");
}
}
bool RnpJob::passProvider(rnp_ffi_t ffi,
void *app_ctx,
rnp_key_handle_t key,
const char *pgp_context,
char buf[],
size_t buf_len)
{
if (strcmp(pgp_context, "protect")) {
return false;
}
strncpy(buf, "password", buf_len);
return true;
}
void RnpJob::loadKeyFile(QSet<QString> *result_fingerprints, const QString path, const uint32_t flags)
{
qDebug() << "[RnpJob] Load keyring at" << path;
rnp_input_t input = NULL;
if (QFileInfo::exists(this->pubringPath())) {
auto ret = rnp_input_from_path(&input, path.toLocal8Bit().constData());
char *json = NULL;
if (ret == RNP_SUCCESS) {
ret = rnp_import_keys(this->m_ffi,
input,
flags,
&json);
}
QJsonDocument json_document = QJsonDocument::fromJson(json);
qDebug() << "[RnpJob] json" << json_document;
if (result_fingerprints) {
foreach (const QJsonValue fingerprint, json_document.object()["keys"].toArray()) {
qDebug() << "[RnpJob] Add fingerprint" << fingerprint["fingerprint"].toString();
result_fingerprints->insert(fingerprint["fingerprint"].toString());
}
}
rnp_input_destroy(input);
rnp_buffer_destroy(json);
terminateOnError(ret);
qDebug() << "[RnpJob] Keyring loaded successfully";
} else {
qDebug() << "[RnpJob] Keyring" << path << "not found";
}
}
void RnpJob::loadPubKeyring(QSet<QString> *result_fingerprints = NULL)
{
this->loadKeyFile(result_fingerprints, this->pubringPath(), RNP_LOAD_SAVE_PUBLIC_KEYS);
}
void RnpJob::loadSecKeyring(QSet<QString> *result_fingerprints = NULL)
{
this->loadKeyFile(result_fingerprints, this->secringPath(), RNP_LOAD_SAVE_SECRET_KEYS);
}
void RnpJob::loadFullKeyring(QSet<QString> *result_fingerprints = NULL)
{
this->loadPubKeyring(result_fingerprints);
this->loadSecKeyring(result_fingerprints);
}
void RnpJob::saveKeyFile(const QString path, const uint32_t flags)
{
qDebug() << "[RnpJob] Saving keyring at" << path;
rnp_output_t output = NULL;
auto ret = rnp_output_to_file(&output, path.toLocal8Bit().data(), RNP_OUTPUT_FILE_OVERWRITE);
if (ret == RNP_SUCCESS) {
qDebug() << "[ImportKeyJob] Saving key pubring ";
ret = rnp_save_keys(this->m_ffi, RNP_KEYSTORE_GPG, output, flags);
}
if (ret == RNP_SUCCESS) {
ret = rnp_output_finish(output);
}
rnp_output_destroy(output);
terminateOnError(ret);
}
void RnpJob::savePubKeyring()
{
this->saveKeyFile(this->pubringPath(), RNP_LOAD_SAVE_PUBLIC_KEYS);
}
void RnpJob::saveSecKeyring()
{
this->saveKeyFile(this->secringPath(), RNP_LOAD_SAVE_SECRET_KEYS);
}
void RnpJob::saveFullKeyring()
{
this->savePubKeyring();
this->saveSecKeyring();
}

185
plugins/Pass/jobs/rnpjob.h Normal file
View File

@ -0,0 +1,185 @@
#ifndef RNPJOB_H
#define RNPJOB_H
#include <QThread>
#include <QDir>
extern "C" {
#include <rnp/rnp.h>
}
#include <variant>
#define terminateOnError(ret) \
if(ret != RNP_SUCCESS) { \
qDebug() << "[RnpJob] Err : " << ret; \
qDebug() << "[RnpJob] Err Msg : " << rnp_result_to_string(ret); \
emit resultError(ret); \
return; \
} \
/**
* @class RnpJob
* @brief A base class that manages OpenPGP-related tasks using the librnp library.
*
* The RnpJob class serves as an abstraction for performing OpenPGP (RNP) operations, such as
* encryption, decryption, and key management, using the RNP library.
*/
class RnpJob : public QThread
{
Q_OBJECT
signals:
/**
* @brief Signal emitted when an error occurs in the RNP job.
*
* This signal is emitted when an error occurs during an RNP operation, such as key loading
* or encryption/decryption failure. The error code is passed to indicate the specific issue.
*
* @param err The error code returned by the RNP operation.
*/
void resultError(const rnp_result_t err);
private:
/**
* @brief A callback function for providing the passphrase to RNP.
*
* This static function is used as a callback to provide a passphrase to RNP when required
* during key operations such as decryption or signing. It allows the library to continue the
* operation with the necessary passphrase.
*
* @param ffi The RNP FFI handle.
* @param app_ctx The application context, used for accessing application-specific data.
* @param key The key for which the passphrase is required.
* @param pgp_context The context string (e.g., "decrypt").
* @param buf The buffer to fill with the passphrase.
* @param buf_len The length of the buffer.
*
* @return true if the passphrase was successfully provided, false otherwise.
*/
static bool passProvider(rnp_ffi_t ffi,
void *app_ctx,
rnp_key_handle_t key,
const char *pgp_context,
char buf[],
size_t buf_len);
QDir m_rnp_homedir; /**< Directory that contains the keyrings and RNP configuration. */
/**
* @brief Loads a key file into the keyring.
*
* This method loads a key file into the keyring, adding keys specified by their fingerprints.
*
* @param result_fingerprints A set to hold the fingerprints of the keys loaded into the keyring.
* @param path The path to the key file.
* @param flags Flags specifying options for loading keys (e.g., overwrite, secret keys, etc.).
*/
void loadKeyFile(QSet<QString> *result_fingerprints, const QString path, const uint32_t flags);
/**
* @brief Saves keys to the keyring file in the specified directory.
*
* This method saves a keyring to a file. It allows you to specify options such as overwriting
* existing files or including secret keys.
*
* @param path The path to the keyring file where the keys should be saved.
* @param flags Flags specifying options for saving the keys (e.g., overwrite, include secret keys, etc.).
*/
void saveKeyFile(const QString path, const uint32_t flags);
protected:
rnp_ffi_t m_ffi; /**< RNP FFI (Foreign Function Interface) handle, used for interacting with the RNP library. */
/**
* @brief Get the path to the public keyring.
*
* This method returns the file path to the public keyring (where public keys are stored).
* It combines the directory and file name to provide the full path.
*
* @return The path to the public keyring file.
*/
QString pubringPath()
{
return this->m_rnp_homedir.filePath("pubring.pgp");
}
/**
* @brief Get the path to the secret keyring.
*
* This method returns the file path to the secret keyring (where private keys are stored).
* It combines the directory and file name to provide the full path.
*
* @return The path to the secret keyring file.
*/
QString secringPath()
{
return this->m_rnp_homedir.filePath("secring.pgp");
}
/**
* @brief Loads the secret keyring into RNP.
*
* @param result_fingerprints A set that will hold the fingerprints of the loaded secret keys.
*/
void loadSecKeyring(QSet<QString> *result_fingerprints);
/**
* @brief Loads the public keyring into RNP.
*
* @param result_fingerprints A set that will hold the fingerprints of the loaded public keys.
*/
void loadPubKeyring(QSet<QString> *result_fingerprints);
/**
* @brief Loads both the public and secret keyrings into RNP.
*
* @param result_fingerprints A set that will hold the fingerprints of all loaded keys.
*/
void loadFullKeyring(QSet<QString> *result_fingerprints);
/**
* @brief Saves the secret keyring to the RNP homedir.
*
*/
void saveSecKeyring();
/**
* @brief Saves the public keyring to the RNP homedir.
*
*/
void savePubKeyring();
/**
* @brief Saves both the public and secret keyrings to the RNP homedir.
*
*/
void saveFullKeyring();
public:
/**
* @brief Constructs an RnpJob object with the specified RNP home directory.
*
* This constructor initializes the RnpJob instance with the directory that contains the
* keyrings and RNP configuration. Keyring files (pubring.pgp and secring.pgp) will be found
* in this directory.
*
* @param rnp_homedir The directory containing the RNP keyrings and configuration files.
*/
RnpJob(QDir rnp_homedir);
/**
* @brief Destructor for the RnpJob class.
*
* The destructor cleans up any resources used by the RnpJob instance, including releasing
* the RNP FFI handle.
*/
~RnpJob();
void setPassProvider(rnp_password_cb pass_provider_cb)
{
rnp_ffi_set_pass_provider(this->m_ffi, pass_provider_cb, NULL);
}
};
#endif // RNPJOB_H

View File

@ -1,64 +1,248 @@
#include <QUrl> #include <QUrl>
#include <QtCore/QStandardPaths> #include <QtCore/QStandardPaths>
#include <QtCore/QDir> #include <QtCore/QDir>
#include <QDirIterator>
#include <QtConcurrent/QtConcurrent>
#include "error.h"
#include "jobs/decryptjob.h"
#include "jobs/deletekeyjob.h"
#include "jobs/getkeysjob.h"
#include "jobs/importkeyjob.h"
#include "jobs/rmjob.h"
#include "pass.h" #include "pass.h"
#include "gpg.h" #include "passphraseprovider.h"
#include "passkeymodel.h"
Pass::Pass(): m_password_store (QStandardPaths::writableLocation( Pass::Pass():
QStandardPaths::AppDataLocation).append("/.password-store")) m_password_store (QStandardPaths::writableLocation(
{} QStandardPaths::AppDataLocation).append("/.password-store")),
m_gpg_home (QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation).append("/.rnp")),
m_passphrase_provider(&UTPassphraseProvider::get_pass_provider),
m_sem(std::unique_ptr<QSemaphore>(new QSemaphore(1)))
{
this->initGpgHome();
this->initPasswordStore();
QObject::connect(this, &Pass::responsePassphraseDialogPropagate,
&UTPassphraseProvider::instance(), &UTPassphraseProvider::handleResponse);
}
void Pass::init(QObject *window) void Pass::initialize(QObject *window)
{ {
if (!window) { if (!window) {
qFatal("window is invalid. Abording."); qWarning("[Pass] Window should be null only for testing");
}
Gpg::instance()->setWindow(window);
QDir dir(m_password_store);
if (!dir.exists()) {
dir.mkpath(".");
}
qInfo() << "Password Store is :" << m_password_store;
}
void Pass::decrypt(QUrl url)
{
qInfo() << "Decrypting";
auto decrypt_ret = Gpg::instance()->decryptFromFile(url.toLocalFile());
if (decrypt_ret.first) {
qInfo() << "Decrypt Failed";
emit decryptFailed();
} else if (decrypt_ret.second.isNull()) {
qInfo() << "Decrypt Canceled";
emit decryptCanceled();
} else { } else {
qInfo() << "Decrypt OK"; UTPassphraseProvider::instance().setWindow(window);
emit decrypted(decrypt_ret.second);
} }
} }
bool Pass::gpgDeleteKeyId(QString id)
void Pass::initGpgHome()
{ {
qInfo() << "Deleting Key id " << id; // delete gpghome from previous version using GPGME
return !Gpg::instance()->deleteKeyId(id); QString path = QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation).append("/.gpghome");
QDir dir(path);
dir.removeRecursively();
// create gpghome for rnp
QDir dir_gpg_home(this->m_gpg_home);
if (!dir_gpg_home.exists()) {
dir_gpg_home.mkpath(".");
}
qInfo() << "[Pass] GPG Home is :" << m_gpg_home;
} }
bool Pass::gpgImportKeyFromFile(QUrl url) void Pass::initPasswordStore()
{ {
qInfo() << "Importing Key from " << url; QDir dir_password_store(this->m_password_store);
return !Gpg::instance()->importKeysFromFile(url.toLocalFile()); if (!dir_password_store.exists()) {
dir_password_store.mkpath(".");
}
qInfo() << "[Pass] Password Store is :" << m_password_store;
} }
QVariant Pass::gpgGetAllKeysModel() void Pass::lsJob()
{ {
qInfo() << "Getting all key form gpg "; QDirIterator it(this->m_password_store, QStringList() << "*.gpg", QDir::Files, QDirIterator::Subdirectories);
return QVariant::fromValue(PassKeyModel::keysToPassKeyQObjectList( QList<QString> ret;
Gpg::instance()->getAllKeys().second)); while (it.hasNext()) {
QFile f(it.next());
QString fname = f.fileName();
fname.remove(0, this->m_password_store.length() + 1); // remove system path
ret.append(fname);
}
qInfo() << "[Pass] ls Succeed";
emit lsSucceed(ret);
this->m_sem->release(1);
} }
bool Pass::ls()
{
qInfo() << "[Pass] ls";
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
QtConcurrent::run(this, &Pass::lsJob );
return true;
}
bool Pass::show(QUrl url)
{
qInfo() << "[Pass] Show";
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
auto job = new DecryptJob(this->m_gpg_home, url.toLocalFile());
job->setPassProvider(this->m_passphrase_provider);
QObject::connect(job, &DecryptJob::resultError, this, &Pass::slotShowError);
QObject::connect(job, &DecryptJob::resultSuccess, this, &Pass::slotShowSucceed);
connect(job, &DecryptJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
void Pass::slotShowError(rnp_result_t err)
{
qInfo() << "[Pass] Show Failed";
emit showFailed(rnpErrorToErrorCodeShow(err), rnp_result_to_string(err));
this->m_sem->release(1);
}
void Pass::slotShowSucceed(QString encrypted_file_path, QString plain_text)
{
qDebug() << "[Pass] Show Succeed";
QFileInfo file_info(encrypted_file_path);
emit showSucceed(file_info.completeBaseName(), plain_text);
this->m_sem->release(1);
}
bool Pass::deletePasswordStore()
{
qInfo() << "[Pass] Delete Password Store at" << this->m_password_store;
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
auto job = new RmJob(this->m_password_store);
connect(job, &RmJob::resultReady, this, &Pass::slotDeletePasswordStoreResult);
connect(job, &RmJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
void Pass::slotDeletePasswordStoreResult(bool err)
{
if (err) {
qInfo() << "[Pass] Delete Password Store Failed";
emit deletePasswordStoreFailed(static_cast<int>(ErrorCode::Error), "Failed to delete password store");
} else {
qInfo() << "[Pass] Delete Password Store Succeed";
this->initPasswordStore(); // reinit an empty password-store
emit deletePasswordStoreSucceed();
}
this->m_sem->release(1);
}
bool Pass::deleteGPGKey(PassKeyModel* key)
{
qInfo() << "[Pass] Delete GPG key fingerprint " << key->property("keyid").toString();
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
auto job = new DeleteKeyJob(this->m_gpg_home, key->property("fingerprint").toString());
QObject::connect(job, &DeleteKeyJob::resultError, this, &Pass::slotDeleteGPGKeyError);
QObject::connect(job, &DeleteKeyJob::resultSuccess, this, &Pass::slotDeleteGPGKeySucceed);
connect(job, &DeleteKeyJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
void Pass::slotDeleteGPGKeyError(rnp_result_t err)
{
qInfo() << "[Pass] Delete GPG key Failed";
emit deleteGPGKeyFailed(rnpErrorToErrorCodeGeneric(err), rnp_result_to_string(err));
this->m_sem->release(1);
}
void Pass::slotDeleteGPGKeySucceed()
{
qInfo() << "[Pass] Delete GPG key Succesfull";
emit deleteGPGKeySucceed();
this->m_sem->release(1);
}
bool Pass::importGPGKey(QUrl url)
{
qInfo() << "[Pass] Import GPG Key from " << url;
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
auto job = new ImportKeyJob(this->m_gpg_home, url.toLocalFile());
QObject::connect(job, &ImportKeyJob::resultError, this, &Pass::slotImportGPGKeyError);
QObject::connect(job, &ImportKeyJob::resultSuccess, this, &Pass::slotImportGPGKeySucceed);
connect(job, &ImportKeyJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
void Pass::slotImportGPGKeyError(rnp_result_t err)
{
qInfo() << "[Pass] Import GPG Key Failed";
emit importGPGKeyFailed(rnpErrorToErrorCodeImportKeyFile(err), rnp_result_to_string(err));
this->m_sem->release(1);
}
void Pass::slotImportGPGKeySucceed()
{
qInfo() << "[Pass] Import GPG Key Succesfull";
emit importGPGKeySucceed();
this->m_sem->release(1);
}
bool Pass::getAllGPGKeys()
{
qInfo() << "[Pass] Get all GPG Keys";
if (!this->m_sem->tryAcquire(1, 500)) {
qInfo() << "[Pass] A command is already running";
return false;
}
this->m_keyring_model = nullptr;
auto job = new GetKeysJob(this->m_gpg_home);
QObject::connect(job, &GetKeysJob::resultError, this, &Pass::slotGetAllGPGKeysError);
QObject::connect(job, &GetKeysJob::resultSuccess, this, &Pass::slotGetAllGPGKeysSucceed);
connect(job, &ImportKeyJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
void Pass::slotGetAllGPGKeysError(rnp_result_t err)
{
qInfo() << "[Pass] Get all GPG Keys Failed";
this->m_keyring_model = nullptr;
emit getAllGPGKeysFailed(rnpErrorToErrorCodeGeneric(err), rnp_result_to_string(err));
this->m_sem->release(1);
}
void Pass::slotGetAllGPGKeysSucceed(QList<QJsonDocument> result)
{
qInfo() << "[Pass] Get all GPG Keys Succeed";
this->m_keyring_model = std::unique_ptr<PassKeyringModel>(new PassKeyringModel(result));
emit getAllGPGKeysSucceed(this->m_keyring_model.get());
this->m_sem->release(1);
}
void Pass::responsePassphraseDialog(bool cancel, QString passphrase)
{
qDebug() << "[Pass] Propagate responsePassphraseDialog to UTPassphraseProvider";
emit responsePassphraseDialogPropagate(cancel, passphrase);
}

View File

@ -1,38 +1,257 @@
#ifndef PASS_H #ifndef PASS_H
#define PASS_H #define PASS_H
#include <QDebug>
#include <QObject> #include <QObject>
#include <QUrl> #include <QUrl>
#include <QVariant> #include <QVariant>
#include <QSemaphore>
#include <memory>
extern "C" {
#include <rnp/rnp.h>
}
#include "passkeyringmodel.h"
/**
* @class Pass
* @brief A class for managing password storage using GPG encryption.
*
* This class provides functionalities for interacting with password storage, including
* storing, showing, importing, and deleting passwords securely using GPG encryption.
*/
class Pass : public QObject class Pass : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString password_store READ password_store) Q_PROPERTY(QString password_store MEMBER m_password_store WRITE set_password_store )
Q_PROPERTY(QString gpg_home MEMBER m_gpg_home WRITE set_gpg_home )
private: private slots:
QString m_password_store; /**
* @brief Slot to handle the result of a GPG decryption operation (to show password).
* @param err The error that occurred during the operation.
* @param plain_text The decrypted plain text (password).
*/
void slotShowError(rnp_result_t err);
void slotShowSucceed(QString encrypted_file_path, QString plain_text);
void slotDeleteGPGKeyError(rnp_result_t err);
void slotDeleteGPGKeySucceed();
/**
* @brief Slot to handle the error result of a GPG key import operation.
* @param err The error that occurred during the operation.
*/
void slotImportGPGKeyError(rnp_result_t err);
/**
* @brief Slot to handle the succeed result of a GPG key import operation.
*/
void slotImportGPGKeySucceed();
/**
* @brief Slot to handle the result of retrieving all GPG keys.
* @param err The error that occurred during the operation.
*/
void slotGetAllGPGKeysError(rnp_result_t err);
/**
* @brief Slot to handle the succeed result of a GPG key get all keys operation.
*/
void slotGetAllGPGKeysSucceed(QList<QJsonDocument> result);
/**
* @brief Slot to handle the result of a delete Password Store operation.
* @param err True if an error occurred during the operation.
*/
void slotDeletePasswordStoreResult(bool err);
signals: signals:
void decrypted(QString text); // GPG-related signals
void decryptCanceled(); /**
void decryptFailed(); * @brief Emitted when a GPG key is successfully deleted.
*/
void deleteGPGKeySucceed();
/**
* @brief Emitted when a GPG key deletion fails.
* @param message The error message describing the failure.
*/
void deleteGPGKeyFailed(int err, QString message);
/**
* @brief Emitted when a GPG key is successfully imported.
*/
void importGPGKeySucceed();
/**
* @brief Emitted when a GPG key import fails.
* @param message The error message describing the failure.
*/
void importGPGKeyFailed(int err, QString message);
/**
* @brief Emitted when all GPG keys are successfully retrieved.
* @param keys_info The list of retrieved keys.
*/
void getAllGPGKeysSucceed(QObject* keys_info);
/**
* @brief Emitted when retrieving GPG keys fails.
* @param message The error message describing the failure.
*/
void getAllGPGKeysFailed(int err, QString message);
// Pass-related signals
/**
* @brief Emitted to propagate passphrase dialog response.
* @param cancel Whether the dialog was cancelled.
* @param passphrase The passphrase entered, if not cancelled.
*/
void responsePassphraseDialogPropagate(bool cancel, QString passphrase);
/**
* @brief Emitted when a password is successfully retrieved and shown.
* @param name The name of the password (e.g., service).
* @param text The password text.
*/
void showSucceed(QString name, QString text);
void lsSucceed(QList<QString>);
/**
* @brief Emitted when showing a password fails.
* @param message The error message describing the failure.
*/
void showFailed(int err, QString message);
/**
* @brief Emitted hen showing a password cancelled.
*/
void showCancelled();
/**
* @brief Emitted when the password store is successfully deleted.
*/
void deletePasswordStoreSucceed();
/**
* @brief Emitted when deleting the password store fails.
* @param message The error message describing the failure.
*/
void deletePasswordStoreFailed(int err, QString message);
private:
QString m_password_store; /**< The path to the password store. */
QString m_gpg_home; /**< The path to the gpg home. */
std::unique_ptr<PassKeyringModel>
m_keyring_model; /**< Meta data on the keyring uid, name, secrecy ... of the availble keys. */
rnp_password_cb m_passphrase_provider; /**< Pointer on passphrase povider for operations using secret keys. */
std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing concurrent operations. */
/**
* @brief Initialize gpg home.
*/
void initGpgHome();
/**
* @brief Initialize password store.
*/
void initPasswordStore();
void lsJob();
public: public:
/**
* @brief Constructs the Pass object.
*/
Pass(); Pass();
~Pass() override = default;
QString password_store() const /**
* @brief Set the path to the password store.
* @param The path to the password store.
*/
void set_password_store(QString password_store)
{ {
return m_password_store; qInfo() << "[Pass] Password Store changed to :" << password_store;
this->m_password_store = password_store;
};
/**
* @brief Set the path to the gpg hom.
* @param The path to the gpg hom
*/
void set_gpg_home(QString gpg_home)
{
qInfo() << "[Pass] GPG Home changed to :" << gpg_home;
this->m_gpg_home = gpg_home;
};
void set_passphrase_provider(rnp_password_cb passphrase_provider)
{
this->m_passphrase_provider = passphrase_provider;
} }
Q_INVOKABLE void init(QObject *window); /**
Q_INVOKABLE void decrypt(QUrl url); * @brief Initializes the Pass object with the given window.
Q_INVOKABLE bool gpgDeleteKeyId(QString id); * @param window The QObject window to interact with.
Q_INVOKABLE bool gpgImportKeyFromFile(QUrl url); */
Q_INVOKABLE QVariant gpgGetAllKeysModel(); Q_INVOKABLE void initialize(QObject *window);
// GPG-related methods
/**
* @brief Launch the job to delete the specified GPG key.
* @param key The PassKeyModel to delete.
* @return True if the job was start successfully, false otherwise.
*/
Q_INVOKABLE bool deleteGPGKey(PassKeyModel* key);
/**
* @brief Launch the job to import a GPG key from the given URL.
* @param url The URL to import the GPG key from.
* @return True if the job was start was successfully, false otherwise.
*/
Q_INVOKABLE bool importGPGKey(QUrl url);
/**
* @brief Launch the to retrieve all GPG keys.
* @return True if the job was start was successfully, false otherwise.
*/
Q_INVOKABLE bool getAllGPGKeys();
/**
* @brief Return the response from the passphrase dialog.
* @param cancel Whether the dialog was cancelled.
* @param passphrase The passphrase entered, if not cancelled.
*/
Q_INVOKABLE void responsePassphraseDialog(bool cancel, QString passphrase);
// Password store-related methods
/**
* @brief Get the list of password.
* @return The list of password in the password store.
*/
Q_INVOKABLE bool ls();
/**
* @brief Launch the job to shows the password associated with the specified URL.
* @param url The URL pointing to the password store entry.
* @return True if the job was start successfully, false otherwise.
*/
Q_INVOKABLE bool show(QUrl url);
/**
* @brief Launch the job to delete the password store.
* @return True if if the job was start successfully, false otherwise.
*/
Q_INVOKABLE bool deletePasswordStore();
}; };
#endif #endif

View File

@ -1,74 +0,0 @@
#ifndef PASSKEYMODEL_H
#define PASSKEYMODEL_H
#include <QObject>
#include <gpgme++/key.h>
using namespace GpgME;
class PassKeyModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString uid READ uid WRITE setUid NOTIFY uidChanged MEMBER m_uid)
Q_PROPERTY(bool secret READ secret WRITE setSecret NOTIFY secretChanged MEMBER m_secret)
Q_PROPERTY(bool expired READ expired WRITE setExpired NOTIFY expiredChanged MEMBER m_expired)
QString m_uid;
bool m_secret;
bool m_expired;
public:
PassKeyModel(QString uid, bool secret, bool expired):
m_uid(uid),
m_secret(secret),
m_expired(expired)
{};
PassKeyModel(Key key):
PassKeyModel(QString::fromUtf8(key.keyID()), key.hasSecret(), key.isExpired())
{};
static QList<QObject *> keysToPassKeyQObjectList(std::vector<Key> keys)
{
QList<QObject *> r;
std::for_each(keys.begin(), keys.end(), [&r](Key k) {
r.append(new PassKeyModel(k));
});
return r;
};
QString uid() const
{
return m_uid;
};
bool secret() const
{
return m_secret;
};
bool expired() const
{
return m_expired;
};
void setUid(QString uid)
{
m_uid = uid;
emit uidChanged(uid);
}
void setSecret(bool secret)
{
m_secret = secret;
emit secretChanged(secret);
}
void setExpired(bool expired)
{
m_expired = expired;
emit expiredChanged(expired);
}
signals:
void uidChanged(QString);
void secretChanged(bool);
void expiredChanged(bool);
};
#endif

View File

@ -0,0 +1,129 @@
#ifndef PASSKEYRINGMODEL_H
#define PASSKEYRINGMODEL_H
#include <QDebug>
#include <QObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QSet>
/**
* @class PassKeyModel
* @brief A model representing a GPG (GNU Privacy Guard) key.
*
*/
class PassKeyModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString fingerprint MEMBER m_fingerprint CONSTANT)
Q_PROPERTY(QString keyid MEMBER m_keyid CONSTANT)
Q_PROPERTY(QVariant userids MEMBER m_userids CONSTANT)
Q_PROPERTY(bool hasSecret MEMBER m_hasSecret CONSTANT)
private:
QString m_fingerprint; /**< The fingerprint of the GPG key, used to uniquely identify the key. */
QString m_keyid; /**< The unique ID associated with the GPG key. */
QVariant m_userids; /**< A list of user IDs associated with the GPG key. */
bool m_hasSecret; /**< Indicates whether the GPG key has an associated secret key. */
public:
/**
* @brief Constructs a PassKeyModel object using the provided GPG key information.
*
* This constructor initializes the PassKeyModel based on a JSON document containing GPG key data.
* The key data typically includes the key's fingerprint, key ID, associated user IDs, and secret key status.
*
* @param key_info A JSON document containing the GPG key data.
*/
PassKeyModel(QJsonDocument key_info)
{
this->m_fingerprint = key_info["fingerprint"].toString();
qDebug() << "[PassKeyModel] fingerprint : " << this->m_fingerprint;
this->m_keyid = key_info["keyid"].toString();
qDebug() << "[PassKeyModel] keyid : " << this->m_keyid;
auto user_ids_json_array = key_info["userids"].toArray();
auto userids = QList<QString>();
for (auto i = user_ids_json_array.begin(), end = user_ids_json_array.end(); i != end; ++i) {
userids.append((*i).toString());
}
this->m_userids = QVariant(userids);
qDebug() << "[PassKeyModel] userids : " << this->m_userids;
this->m_hasSecret = key_info["secret key"]["present"].toBool();
qDebug() << "[PassKeyModel] hasSecret : " << this->m_hasSecret;
}
};
/**
* @class PassKeyringModel
* @brief A model representing a collection of GPG keys.
*
* This class serves as a container for multiple GPG keys, typically representing an entire
* keyring. It provides functionality to manage and retrieve keys, such as fetching all keys
* in the keyring and determining the length of the keyring.
*
* The class also includes logic to distinguish between primary and sub keys, with an option
* to ignore subkeys if desired.
*/
class PassKeyringModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QList<QObject *> keys MEMBER m_keys CONSTANT)
Q_PROPERTY(int length READ length CONSTANT)
private:
QList<QObject *> m_keys; /**< A list of PassKeyModel objects representing the keys in the keyring. */
public:
/**
* @brief Constructs a PassKeyringModel from a list of GPG key JSON documents.
*
* This constructor initializes the PassKeyringModel by parsing a list of JSON documents
* that represent multiple GPG keys. It filters out subkeys and only retains primary keys
* for inclusion in the keyring.
*
* @param key_infos A list of JSON documents representing GPG keys.
*/
PassKeyringModel(QList<QJsonDocument> key_infos)
{
for (auto i = key_infos.begin(), end = key_infos.end(); i != end; ++i) {
qDebug() << "[PassKeyringModel]" << *i;
// Ignore subkeys and only add primary keys to the model.
if ((*i)["primary key grip"].isUndefined()) {
this->m_keys.append(new PassKeyModel(*i));
} else {
qDebug() << "[PassKeyringModel] Subkey info " << (*i)["keyid"].toString() << "ignored";
}
}
}
/**
* @brief Destructor for PassKeyringModel.
*
* Cleans up dynamically allocated PassKeyModel objects within the keyring.
*/
~PassKeyringModel()
{
qDeleteAll(this->m_keys);
}
/**
* @brief Retrieves the number of keys in the keyring.
*
* This function returns the number of primary keys present in the keyring.
*
* @return The number of keys in the keyring.
*/
int length()
{
return this->m_keys.length();
}
};
#endif // PASSKEYRINGMODEL_H

View File

@ -1,87 +1,190 @@
#ifndef UTPASSPHRASEPROVIDER_H #ifndef UTPASSPHRASEPROVIDER_H
#define UTPASSPHRASEPROVIDER_H #define UTPASSPHRASEPROVIDER_H
#include <QDebug>
#include <memory>
#include <stdio.h> #include <stdio.h>
#include <QObject> #include <QObject>
#include <QQmlProperty> #include <QQmlProperty>
#include <QEventLoop> #include <QEventLoop>
#include <QSemaphore> #include <QSemaphore>
#include <gpgme++/interfaces/passphraseprovider.h> extern "C" {
#include "gpg.h" #include <rnp/rnp.h>
}
/**
* @class UTPassphraseProvider
class UTPassphraseProvider : public QObject, public PassphraseProvider * @brief A passphrase provider for GPG operations that interacts with a QML dialog.
*
* This class is used to prompt the user for a passphrase through a QML-based dialog. It manages
* the passphrase entry process and signals whether the user has provided a passphrase or canceled
* the operation.
*/
class UTPassphraseProvider : public QObject
{ {
Q_OBJECT Q_OBJECT
private:
std::unique_ptr<QEventLoop> m_loop;
std::unique_ptr<QSemaphore> m_sem;
char *m_passphrase;
bool m_canceled;
public slots: public slots:
void handleResponse(bool canceled, QString p) /**
* @brief Slot to handle the user's response from the passphrase dialog.
*
* This slot is called when the user provides a passphrase or cancels the passphrase entry.
* If the user provides a passphrase, it is stored; if the user cancels, a cancellation flag
* is set.
*
* @param canceled Indicates whether the user canceled the passphrase entry.
* @param passphrase The passphrase entered by the user (if not canceled).
*/
void handleResponse(bool canceled, QString passphrase)
{ {
if (!canceled) qDebug() << "[UTPassphraseProvider] Call handleResponse";
gpgrt_asprintf(&m_passphrase, "%s", p.toUtf8().constData()); if (!canceled) {
else this->m_canceled = false;
m_canceled = true; this->m_passphrase = passphrase;
m_loop->quit(); }
unlockEventLoop();
}; };
signals:
/**
* @brief Signal to unlock the event loop.
*
* This signal is emitted when the passphrase has been entered or the operation has been canceled,
* causing the event loop waiting for the response to exit.
*/
void unlockEventLoop();
public:
UTPassphraseProvider(): private:
m_loop(std::unique_ptr<QEventLoop>(new QEventLoop)), /**
m_sem(std::unique_ptr<QSemaphore>(new QSemaphore(1))), * @brief Private constructor for singleton pattern.
m_passphrase(nullptr), *
m_canceled(false) * Initializes the passphrase provider with a semaphore to manage access, and a flag to indicate
* whether the operation was canceled.
*
* @param parent Parent QObject (default is nullptr).
*/
explicit UTPassphraseProvider(QObject * parent = nullptr)
: m_sem(std::make_unique<QSemaphore>(1)),
m_passphrase(QString::Null()),
m_canceled(true)
{} {}
char *getPassphrase( const char *useridHint, QObject *m_window; /**< The window object that triggers the QML dialog. */
const char *description, std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing access to the passphrase entry process. */
bool previousWasBad, QString m_passphrase; /**< The passphrase provided by the user. */
bool &canceled ) Q_DECL_OVERRIDE { bool m_canceled; /**< Flag indicating whether the passphrase operation was canceled. */
if (!m_sem->tryAcquire(1, 3000))
{ public:
qWarning() << "Cannot acquire UTPassphraseProvider semaphore."; ~UTPassphraseProvider() = default;
canceled = true;
return nullptr; /**
* @brief Gets the singleton instance of UTPassphraseProvider.
*
* This method ensures that only one instance of the passphrase provider exists throughout the application.
*
* @return The singleton instance of UTPassphraseProvider.
*/
static UTPassphraseProvider &instance()
{
static UTPassphraseProvider instance;
return instance;
}
UTPassphraseProvider(UTPassphraseProvider const &) = delete; /**< Prevents copying of the instance. */
void operator=(UTPassphraseProvider const &) = delete; /**< Prevents assignment of the instance. */
/**
* @brief Callback function to retrieve the passphrase for GPG operations.
*
* This static method is called by the GPG library when it requires a passphrase for a specific key operation.
* It triggers a QML dialog to prompt the user for the passphrase and waits for a response.
*
* @param ffi The RNP FFI instance.
* @param app_ctx provided by application
* @param key the key, if any, for which the password is being requested.
* Note: this key handle should not be held by the application,
* it is destroyed after the callback. It should only be used to
* retrieve information like the userids, grip, etc.
* @param pgp_context a descriptive string on why the password is being
* requested, may have one of the following values:
* - "add subkey": add subkey to the encrypted secret key
* - "add userid": add userid to the encrypted secret key
* - "sign": sign data
* - "decrypt": decrypt data using the encrypted secret key
* - "unlock": temporary unlock secret key (decrypting its fields), so it may be used
* later without need to decrypt
* - "protect": encrypt secret key fields
* - "unprotect": decrypt secret key fields, leaving those in a raw format
* - "decrypt (symmetric)": decrypt data, using the password
* - "encrypt (symmetric)": encrypt data, using the password
* @param buf to which the callback should write the returned password, NULL terminated.
* @param buf_len the size of buf
*
* @return true if a password was provided, false otherwise
*/
static bool
get_pass_provider( rnp_ffi_t ffi,
void *app_ctx,
rnp_key_handle_t key,
const char *pgp_context,
char buf[],
size_t buf_len)
{
qDebug() << "[UTPassphraseProvider] Call the getPassphrase";
if (!UTPassphraseProvider::instance().m_window) {
qWarning() << "[UTPassphraseProvider] Aborting : window is not set";
return false;
} }
m_passphrase = nullptr; if (!UTPassphraseProvider::instance().m_sem->tryAcquire(1, 500)) {
m_canceled = false; qWarning() << "[UTPassphraseProvider] Aborting : Cannot acquire UTPassphraseProvider semaphore";
return false;
}
qDebug() << "Call the QML Dialog Passphrase Provider"; UTPassphraseProvider::instance().m_passphrase = QString::Null();
UTPassphraseProvider::instance().m_canceled = true;
qDebug() << "[UTPassphraseProvider] Call the QML Dialog Passphrase Provider";
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
Gpg::instance()->getWindow(), "callPassphraseDialog", UTPassphraseProvider::instance().m_window, "callPassphraseDialog",
Q_ARG(QVariant, useridHint), Q_ARG(QVariant, "useridHint"), // TODO
Q_ARG(QVariant, description), Q_ARG(QVariant, "description"), // TODO
Q_ARG(QVariant, previousWasBad) Q_ARG(QVariant, "previousWasBad") // TODO
); );
qDebug() << "Waiting for response"; qDebug() << "[UTPassphraseProvider] Waiting for response";
QObject::connect( QEventLoop loop;
Gpg::instance()->getWindow(), SIGNAL(responsePassphraseDialog(bool, QString)), QObject::connect(&UTPassphraseProvider::instance(), &UTPassphraseProvider::unlockEventLoop, &loop, &QEventLoop::quit);
this, SLOT(handleResponse(bool, QString)) loop.exec();
);
m_loop->exec();
qDebug() << "Prepare Returns"; qDebug() << "[UTPassphraseProvider] Prepare Returns";
char *ret; auto ret = false;
gpgrt_asprintf(&ret, "%s", m_passphrase); if (!UTPassphraseProvider::instance().m_canceled) {
canceled = m_canceled; strncpy(buf, UTPassphraseProvider::instance().m_passphrase.toLocal8Bit().data(), buf_len);
ret = true;
};
qDebug() << "Clean"; qDebug() << "[UTPassphraseProvider] Clean Up";
if (m_passphrase) UTPassphraseProvider::instance().m_passphrase = QString::Null();
{ UTPassphraseProvider::instance().m_canceled = true;
free(m_passphrase); UTPassphraseProvider::instance().m_sem->release(1);
}
m_canceled = false;
m_sem->release(1);
return ret; return ret;
}; }
/**
* @brief Sets the window object that triggers the passphrase dialog.
*
* This method allows the passphrase provider to know which window should invoke the QML dialog.
*
* @param window The window object to set.
*/
void setWindow(QObject* window)
{
this->m_window = window;
}
}; };
#endif
#endif // UTPASSPHRASEPROVIDER_H

View File

@ -5,6 +5,7 @@ set(
SRC SRC
plugin.cpp plugin.cpp
utils.cpp utils.cpp
jobs/unzipjob.cpp
) )
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)

View File

@ -0,0 +1,56 @@
#include <QFile>
#include <QDir>
#include <QUrl>
#include <QtCore/QStandardPaths>
#include <quazip5/JlCompress.h>
#include "qdebug.h"
#include "unzipjob.h"
UnzipJob::UnzipJob(QUrl zip_url, QDir dir_out):
m_zip_url(zip_url),
m_dir_out(dir_out)
{
this->setObjectName("UnzipJob");
}
void UnzipJob::run()
{
auto tmp_dir_path = QStandardPaths::writableLocation(
QStandardPaths::CacheLocation).append("/unzip");
QDir tmp_dir(tmp_dir_path);
tmp_dir.removeRecursively();
tmp_dir.mkpath(".");
qDebug() << "Temp dir path is " << tmp_dir_path;
auto status = !JlCompress::extractDir(
this->m_zip_url.toLocalFile(),
tmp_dir_path
).isEmpty();
if (!status) {
tmp_dir.removeRecursively();
emit resultReady(false);
return;
}
qDebug() << "Guessing if it should remove a single root folder";
QStringList files_in_tmp_dir = tmp_dir.entryList(QDir::AllEntries | QDir::Hidden |
QDir::NoDotAndDotDot);
auto dir_import_path =
files_in_tmp_dir.length() == 1 ?
tmp_dir_path.append("/" + files_in_tmp_dir.first()) : tmp_dir_path;
qDebug() << "Final imported tmp path dir is " << dir_import_path;
qDebug() << "Removing destination";
this->m_dir_out.removeRecursively();
qDebug() << "Moving zip content to destination";
QDir dir;
qDebug() << dir_import_path << " to " << this->m_dir_out;
auto ret = dir.rename(dir_import_path, this->m_dir_out.absolutePath());
tmp_dir.removeRecursively();;
emit resultReady(!ret);
}

View File

@ -0,0 +1,49 @@
#ifndef RMJOB_H
#define RMJOB_H
#include "qurl.h"
#include <QThread>
#include <QDir>
/**
* @class RmJob
* @brief A class to handle removing recursively a path in a separate thread.
*
*/
class UnzipJob : public QThread
{
Q_OBJECT
/**
* @brief The main function that performs the unzip operation.
*
* Handles the process of unziping a archive to a target directory.
*/
void run() override;
signals:
/**
* @brief Signal emitted when the unzip operation is complete.
*
* @param err A boolean indicating whether an error occurred during unzipping.
* `true` if an error occurred, `false` if the clone was successful.
*/
void resultReady(const bool err);
private:
QUrl m_zip_url; ///< The url of the archive.
QDir m_dir_out; ///< The directory where the content of the archive will be unzip.
public:
/**
* @brief Constructor for the UnzipJob class.
*
* Initializes the UnzipJob with the specified target path to be removed.
*
* @param zip_url Url of the archive to be unzip.
* @param dir_out Target directory where the content of the archive must be extracted.
*/
UnzipJob(QUrl zip_url, QDir dir_out);
};
#endif // RMJOB_H

View File

@ -1,51 +1,49 @@
#include <QFile>
#include <QDir> #include <QDir>
#include <QUrl> #include <QSemaphore>
#include <QtCore/QStandardPaths>
#include <quazip5/JlCompress.h>
#include "jobs/unzipjob.h"
#include "utils.h" #include "utils.h"
Utils::Utils():
m_sem(std::unique_ptr<QSemaphore>(new QSemaphore(1)))
{}
bool Utils::unzip(QUrl zip_url, QString dir_out_path) bool Utils::unzip(QUrl zip_url, QString dir_out_path)
{ {
auto tmp_dir_path = QStandardPaths::writableLocation( if (!this->m_sem->tryAcquire(1, 500)) {
QStandardPaths::CacheLocation).append("/unzip");
QDir tmp_dir(tmp_dir_path);
tmp_dir.removeRecursively();
tmp_dir.mkpath(".");
qDebug() << "Temp dir path is " << tmp_dir_path;
auto status = !JlCompress::extractDir(
zip_url.toLocalFile(),
tmp_dir_path
).isEmpty();
if (!status) {
tmp_dir.removeRecursively();
return false; return false;
} }
qInfo() << "[Utils] Unzip path " << zip_url << " to " << dir_out_path;
auto job = new UnzipJob(zip_url, QDir(dir_out_path));
connect(job, &UnzipJob::resultReady, this, &Utils::unzipResult);
connect(job, &UnzipJob::finished, job, &QObject::deleteLater);
job->start();
return true;
}
qDebug() << "Guessing if it should remove a single root folder"; void Utils::unzipResult(bool err)
QStringList files_in_tmp_dir = tmp_dir.entryList(QDir::AllEntries | QDir::Hidden | {
QDir::NoDotAndDotDot);
auto dir_import_path = qDebug() << "[Utils] Unzip Result";
files_in_tmp_dir.length() == 1 ? if (err) {
tmp_dir_path.append("/" + files_in_tmp_dir.first()) : tmp_dir_path; qInfo() << "[Utils] Unzip Failed";
qDebug() << "Final imported tmp path dir is " << dir_import_path; emit unzipFailed();
qDebug() << "Removing destination"; } else {
QDir dir_out(dir_out_path); qInfo() << "[Utils] Unzip Succeed";
dir_out.removeRecursively(); emit unzipSucceed();
}
this->m_sem->release(1);
}
qDebug() << "Moving zip content to destination";
QDir dir; QString Utils::manifestPath()
qDebug() << dir_import_path << " to " << dir_out_path; {
auto ret = dir.rename(dir_import_path, dir_out_path); auto path = QDir(QDir::currentPath()).filePath("manifest_.json");
tmp_dir.removeRecursively();; qInfo() << "[Utils] Manifest path : " << path;
return ret; return path;
} }
bool Utils::rmFile(QUrl file_url) bool Utils::rmFile(QUrl file_url)
@ -58,3 +56,12 @@ bool Utils::rmDir(QUrl dir_url)
QDir dir(dir_url.toLocalFile()); QDir dir(dir_url.toLocalFile());
return dir.removeRecursively(); return dir.removeRecursively();
} }
bool Utils::fileExists(QUrl path)
{
QString p = path.toString();
auto ret = QFileInfo::exists(p) && QFileInfo(p).isFile();
qDebug() << "[Utils]" << path << "existing file :" << ret;
return ret;
}

View File

@ -4,18 +4,90 @@
#include <QObject> #include <QObject>
#include <QUrl> #include <QUrl>
#include <QQuickWindow> #include <QQuickWindow>
#include <memory>
#include <QSemaphore>
/**
* @class Utils
* @brief A utility class that provides helper functions for file and directory operations.
*
* The `Utils` class contains various helper methods such as for managing files and directories, including unzipping files
* and removing files or directories.
*/
class Utils : public QObject class Utils : public QObject
{ {
Q_OBJECT Q_OBJECT
public:
Utils() = default;
~Utils() override = default;
private slots:
/**
* @brief Slot to handle the result of a unzip operation.
* @param err True if an error occurred during the operation.
*/
void unzipResult(bool err);
signals:
/**
* @brief Emitted when the archive is successfully extracted.
*/
void unzipSucceed();
/**
* @brief Emitted when the unzipping operation fails.
*/
void unzipFailed();
private:
std::unique_ptr<QSemaphore> m_sem; /**< Semaphore for managing concurrent operations. */
public:
/**
* @brief Constructor for the Utils class.
*/
Utils();
/**
* @brief Start a job to unzips a ZIP file to the specified output directory.
*
* @param zip_url The URL of the ZIP file to unzip.
* @param dir_out The output directory where the contents of the ZIP file should be extracted.
* @return `true` if the unzipping job is started successfullly, `false` otherwise.
*/
Q_INVOKABLE bool unzip(QUrl zip_url, QString dir_out); Q_INVOKABLE bool unzip(QUrl zip_url, QString dir_out);
/**
* @brief Retrieves the path to the manifest data.
*
* This function returns the full path to the manifest file used by the application.
*
* @return A QString containing the manifest file path.
*/
Q_INVOKABLE QString manifestPath();
/**
* @brief Removes a file located at the specified URL.
*
* @param file_url The URL of the file to remove.
* @return `true` if the file was successfully removed; `false` otherwise.
*/
Q_INVOKABLE bool rmFile(QUrl file_url); Q_INVOKABLE bool rmFile(QUrl file_url);
/**
* @brief Removes a directory located at the specified URL.
*
* @param dir_url The URL of the directory to remove.
* @return `true` if the directory was successfully removed; `false` otherwise.
*/
Q_INVOKABLE bool rmDir(QUrl dir_url); Q_INVOKABLE bool rmDir(QUrl dir_url);
/**
* @brief Verify that file exists at the specified URL.
*
* @param path The URL of the file to verfidy.
* @return `true` if the file exist; `false` otherwise.
*/
Q_INVOKABLE bool fileExists(QUrl path);
}; };
#endif #endif

268
po/nl.po Normal file
View File

@ -0,0 +1,268 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the utpass.qrouland package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: utpass.qrouland\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-04 17:49+0100\n"
"PO-Revision-Date: 2025-02-12 22:20+0100\n"
"Last-Translator: Heimen Stoffels <vistausss@fastmail.com>\n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n"
#: ../qml/dialogs/ErrorDialog.qml:12
msgid "Error !"
msgstr "Foutmelding!"
#: ../qml/dialogs/ErrorDialog.qml:16
msgid "Close"
msgstr "Sluiten"
#: ../qml/dialogs/PassphraseDialog.qml:15
msgid "Authentication required"
msgstr "Verificatie vereist"
#: ../qml/dialogs/PassphraseDialog.qml:16
msgid "Enter passphrase:"
msgstr "Voer de toegangszin in:"
#: ../qml/dialogs/PassphraseDialog.qml:21
msgid "passphrase"
msgstr "toegangszin"
#: ../qml/dialogs/PassphraseDialog.qml:29
#: ../qml/dialogs/SimpleValidationDialog.qml:8
msgid "Ok"
msgstr "Oké"
#: ../qml/dialogs/PassphraseDialog.qml:41
#: ../qml/dialogs/SimpleValidationDialog.qml:28
msgid "Cancel"
msgstr "Annuleren"
#: ../qml/dialogs/SuccessDialog.qml:12
msgid "Success !"
msgstr "Goedgekeurd!"
#: ../qml/dialogs/SuccessDialog.qml:16
msgid "OK"
msgstr "Oké"
#: ../qml/pages/Info.qml:62
msgid "<b>Version</b>"
msgstr "<b>Versie</b>"
#: ../qml/pages/Info.qml:83
msgid "<b>Maintainer</>"
msgstr "<b>Onderhouder</b>"
#: ../qml/pages/Info.qml:110
msgid "Suggest improvement(s) or report a bug(s)"
msgstr "Ideeën delen of bugs melden"
#: ../qml/pages/Info.qml:115
msgid "Access to the source code"
msgstr "Broncode bekijken"
#: ../qml/pages/Info.qml:123
msgid "Released under the terms of the GNU GPL v3"
msgstr "Uitgebracht onder de GNU GPLv3-licentie"
#: ../qml/pages/Info.qml:132 ../qml/pages/headers/MainHeader.qml:33
msgid "Info"
msgstr "Informatie"
#: ../qml/pages/PasswordList.qml:45
msgid "No password found"
msgstr "Er zijn geen wachtwoorden beschikbaar"
#: ../qml/pages/PasswordList.qml:58
msgid "You can import a password store by cloning or"
msgstr "Importeer wachtwoorden door te klonen of een zipbestand"
#: ../qml/pages/PasswordList.qml:65
msgid "importing a password store zip in the settings"
msgstr "te importeren vanuit de instellingen"
#: ../qml/pages/PasswordList.qml:100
msgid "Decryption failed !"
msgstr "Het ongrendelen is mislukt!"
#: ../qml/pages/PasswordList.qml:114
msgid "Back"
msgstr "Terug"
#: ../qml/pages/PasswordList.qml:121 ../qml/pages/headers/MainHeader.qml:9
#: ../qml/pages/headers/StackHeader.qml:9 UTPass.desktop.in.h:1
msgid "UTPass"
msgstr "UTPass"
#: ../qml/pages/headers/MainHeader.qml:26 ../qml/pages/settings/Settings.qml:75
msgid "Settings"
msgstr "Instellingen"
#: ../qml/pages/headers/MainHeader.qml:57
msgid "Search"
msgstr "Zoeken"
#: ../qml/pages/settings/DeleteRepo.qml:42
#: ../qml/pages/settings/Settings.qml:58
msgid "Delete Password Store"
msgstr "Wachtwoordopslag verwijderen"
#: ../qml/pages/settings/DeleteRepo.qml:55
msgid "You're are about to delete<br>the current Password Store.<br>Continue ?"
msgstr ""
"Je staat op het punt om de huidige<br>wachtwoordopslag te verwijderen."
"<br>Weet je het zeker?"
#: ../qml/pages/settings/DeleteRepo.qml:56
#: ../qml/pages/settings/ImportZip.qml:66
#: ../qml/pages/settings/InfoKeys.qml:174
#: ../qml/pages/settings/git/ImportGitClone.qml:56
msgid "Yes"
msgstr "Ja"
#: ../qml/pages/settings/DeleteRepo.qml:69
msgid "Password Store removal failed !"
msgstr "Het verwijderen is mislukt!"
#: ../qml/pages/settings/DeleteRepo.qml:78
msgid "Password Store deleted !"
msgstr "De opslag is verwijderd!"
#: ../qml/pages/settings/DeleteRepo.qml:90
#: ../qml/pages/settings/InfoKeys.qml:216
msgid "Info Keys"
msgstr "Informatiesleutels"
#: ../qml/pages/settings/ImportKeyFile.qml:61
msgid "Key import failed !"
msgstr "Het importeren is mislukt!"
#: ../qml/pages/settings/ImportKeyFile.qml:70
msgid "Key successfully imported !"
msgstr "De sleutels zijn geïmporteerd!"
#: ../qml/pages/settings/ImportKeyFile.qml:81
msgid "GPG Key Import"
msgstr "Gpg-sleutelimport"
#: ../qml/pages/settings/ImportZip.qml:65
msgid ""
"Importing a new zip will delete<br>any existing password store!<br>Continue ?"
msgstr ""
"Door een zipbestand te importeren<br>wordt de huidige opslag gewist!<br>Weet "
"je het zeker?"
#: ../qml/pages/settings/ImportZip.qml:79
msgid "Password store import failed !"
msgstr "Het importeren is mislukt!"
#: ../qml/pages/settings/ImportZip.qml:88
#: ../qml/pages/settings/git/ImportGitClone.qml:78
msgid "Password store sucessfully imported !"
msgstr "De wachtwoorden zijn geïmporteerd!"
#: ../qml/pages/settings/ImportZip.qml:100
msgid "Zip Password Store Import"
msgstr "Zipbestandsimport"
#: ../qml/pages/settings/InfoKeys.qml:47
msgid "No key found"
msgstr "Er zijn geen sleutels aangetroffen"
#: ../qml/pages/settings/InfoKeys.qml:83
msgid "Key ID :"
msgstr "Sleutel-id:"
#: ../qml/pages/settings/InfoKeys.qml:124
msgid "User IDs : "
msgstr "Gebruikers-id's: "
#: ../qml/pages/settings/InfoKeys.qml:151
msgid "Delete this key"
msgstr "Sleutel verwijderen"
#: ../qml/pages/settings/InfoKeys.qml:173
msgid "You're are about to delete<br>%1.<br>Continue ?"
msgstr "Je staat op het punt om<br>%1 te verwijderen.<br>Weet je het zeker?"
#: ../qml/pages/settings/InfoKeys.qml:187
msgid "Key removal failed !"
msgstr "Het verwijderen is mislukt!"
#: ../qml/pages/settings/InfoKeys.qml:196
msgid "Key successfully deleted !"
msgstr "De sleutel is verwijderd!"
#: ../qml/pages/settings/InfoKeys.qml:208
msgid "An Error occured getting GPG keys !"
msgstr "Er is een fout opgetreden tijdens het ophalen van de sleutels!"
#: ../qml/pages/settings/Settings.qml:23
msgid "GPG"
msgstr "Gpg"
#: ../qml/pages/settings/Settings.qml:29
msgid "Import a GPG key file"
msgstr "Gpg-sleutelbestand importeren"
#: ../qml/pages/settings/Settings.qml:34
msgid "Show GPG keys"
msgstr "Gpg-sleutels bekijken"
#: ../qml/pages/settings/Settings.qml:42
msgid "Password Store"
msgstr "Wachtwoordopslag"
#: ../qml/pages/settings/Settings.qml:48
msgid "Import a Password Store using Git"
msgstr "Wachtwoordopslag importeren met Git"
#: ../qml/pages/settings/Settings.qml:53
msgid "Import a Password Store Zip"
msgstr "Wachtwoordopslag importeren uit zipbestand"
#: ../qml/pages/settings/Settings.qml:67
msgid "Warning: importing delete any exiting Password Store"
msgstr "Waarschuwing: de huidige opslag wordt hierdoor gewist"
#: ../qml/pages/settings/git/GitCloneHttp.qml:16
#: ../qml/pages/settings/git/GitCloneHttpAuth.qml:16
msgid "Repo Url"
msgstr "Repo-url"
#: ../qml/pages/settings/git/GitCloneHttp.qml:40
#: ../qml/pages/settings/git/GitCloneHttpAuth.qml:60
msgid "Clone"
msgstr "Klonen"
#: ../qml/pages/settings/git/GitCloneHttpAuth.qml:35
#: ../qml/pages/settings/git/GitCloneHttpAuth.qml:46
msgid "Password"
msgstr "Wachtwoord"
#: ../qml/pages/settings/git/ImportGitClone.qml:55
msgid ""
"Importing a git repo will delete<br>any existing password store!"
"<br>Continue ?"
msgstr ""
"Door een git-repo te importeren<br>wordt de huidige opslag gewist!<br>Weet "
"je het zeker?"
#: ../qml/pages/settings/git/ImportGitClone.qml:69
msgid "An error occured during git clone !"
msgstr "Er is een fout opgetreden tijdens het klonen!"
#: ../qml/pages/settings/git/ImportGitClone.qml:90
msgid "Git Clone Import"
msgstr "Git-kloonimport"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: utpass.qrouland\n" "Project-Id-Version: utpass.qrouland\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-13 20:49+0100\n" "POT-Creation-Date: 2025-03-14 10:08+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,15 +17,69 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n" "Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: ../qml/components/FileDir.qml:72 #: ../qml/components/GitCloneHttp.qml:22
msgid "Decryption failed !" #: ../qml/components/GitCloneHttpAuth.qml:22
#: ../qml/components/GitCloneSshKey.qml:41
msgid "Repo Url"
msgstr "" msgstr ""
#: ../qml/dialogs/ErrorDialog.qml:12 #: ../qml/components/GitCloneHttp.qml:47
#: ../qml/components/GitCloneHttpAuth.qml:67
#: ../qml/components/GitCloneSshKey.qml:143
msgid "Clone"
msgstr ""
#: ../qml/components/GitCloneHttpAuth.qml:42
#: ../qml/components/GitCloneHttpAuth.qml:53
msgid "Password"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:61
msgid "SSH private key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:70
msgid "Import SSH private key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:79
msgid "Delete SSH private key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:89
msgid "SSH public key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:98
msgid "Delete SSH public key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:107
msgid "Import SSH public key"
msgstr ""
#: ../qml/components/GitCloneSshKey.qml:118
#: ../qml/components/GitCloneSshKey.qml:129
msgid "Passphrase"
msgstr ""
#: ../qml/components/ImportFile.qml:18
msgid "Import succeeded !"
msgstr ""
#: ../qml/components/ImportFile.qml:19
msgid "Import failed !"
msgstr ""
#: ../qml/components/ImportFile.qml:21
msgid "File Imported"
msgstr ""
#: ../qml/dialogs/ErrorDialog.qml:13
msgid "Error !" msgid "Error !"
msgstr "" msgstr ""
#: ../qml/dialogs/ErrorDialog.qml:16 #: ../qml/dialogs/ErrorDialog.qml:17
msgid "Close" msgid "Close"
msgstr "" msgstr ""
@ -42,12 +96,12 @@ msgid "passphrase"
msgstr "" msgstr ""
#: ../qml/dialogs/PassphraseDialog.qml:29 #: ../qml/dialogs/PassphraseDialog.qml:29
#: ../qml/dialogs/SimpleValidationDialog.qml:9 #: ../qml/dialogs/SimpleValidationDialog.qml:8
msgid "Ok" msgid "Ok"
msgstr "" msgstr ""
#: ../qml/dialogs/PassphraseDialog.qml:41 #: ../qml/dialogs/PassphraseDialog.qml:41
#: ../qml/dialogs/SimpleValidationDialog.qml:33 #: ../qml/dialogs/SimpleValidationDialog.qml:28
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
@ -59,162 +113,221 @@ msgstr ""
msgid "OK" msgid "OK"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:61 #: ../qml/pages/Info.qml:62
msgid "<b>Version</b>" msgid "<b>Version</b>"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:82 #: ../qml/pages/Info.qml:83
msgid "<b>Maintainer</>" msgid "<b>Maintainer</>"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:109 #: ../qml/pages/Info.qml:110
msgid "Suggest improvement(s) or report a bug(s)" msgid "Suggest improvement(s) or report a bug(s)"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:114 #: ../qml/pages/Info.qml:115
msgid "Access to the source code" msgid "Access to the source code"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:122 #: ../qml/pages/Info.qml:123
msgid "Released under the terms of the GNU GPL v3" msgid "Released under the terms of the GNU GPL v3"
msgstr "" msgstr ""
#: ../qml/pages/Info.qml:131 ../qml/pages/headers/MainHeader.qml:33 #: ../qml/pages/Info.qml:132 ../qml/pages/headers/MainHeader.qml:38
msgid "Info" msgid "Info"
msgstr "" msgstr ""
#: ../qml/pages/PasswordList.qml:26 #: ../qml/pages/PasswordList.qml:58
msgid "" msgid "Bad passphrase"
"No password found<br>You can import a password store by cloning or importing "
"a zip in the settings"
msgstr "" msgstr ""
#: ../qml/pages/PasswordList.qml:65 #: ../qml/pages/PasswordList.qml:61
msgid "No valid key found"
msgstr ""
#: ../qml/pages/PasswordList.qml:64
msgid "Decryption failed"
msgstr ""
#: ../qml/pages/PasswordList.qml:98
msgid "No password found"
msgstr ""
#: ../qml/pages/PasswordList.qml:111
msgid "You can import a password store by cloning or"
msgstr ""
#: ../qml/pages/PasswordList.qml:118
msgid "importing a password store zip in the settings"
msgstr ""
#: ../qml/pages/PasswordList.qml:195
msgid "Decryption failed !"
msgstr ""
#: ../qml/pages/PasswordList.qml:219
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: ../qml/pages/headers/MainHeader.qml:9 ../qml/pages/headers/StackHeader.qml:9 #: ../qml/pages/PasswordList.qml:226 ../qml/pages/headers/MainHeader.qml:14
#: UTPass.desktop.in.h:1 #: ../qml/pages/headers/StackHeader.qml:9 UTPass.desktop.in.h:1
msgid "UTPass" msgid "UTPass"
msgstr "" msgstr ""
#: ../qml/pages/headers/MainHeader.qml:26 ../qml/pages/settings/Settings.qml:70 #: ../qml/pages/headers/MainHeader.qml:20
msgid "Settings" #: ../qml/pages/headers/MainHeader.qml:62
msgstr ""
#: ../qml/pages/headers/MainHeader.qml:57
msgid "Search" msgid "Search"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:36 #: ../qml/pages/headers/MainHeader.qml:31 ../qml/pages/settings/Settings.qml:73
msgid "Repo Url" msgid "Settings"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:53 #: ../qml/pages/settings/DeleteRepo.qml:43
msgid "Password" #: ../qml/pages/settings/DeleteRepo.qml:93
#: ../qml/pages/settings/Settings.qml:56
msgid "Delete Password Store"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:70 #: ../qml/pages/settings/DeleteRepo.qml:56
msgid "Clone" msgid "You're are about to delete<br>the current Password Store.<br>Continue ?"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:90 #: ../qml/pages/settings/DeleteRepo.qml:57
#: ../qml/pages/settings/ImportGitClone.qml:142
#: ../qml/pages/settings/ImportZip.qml:66
#: ../qml/pages/settings/InfoKeys.qml:174
msgid "Yes"
msgstr ""
#: ../qml/pages/settings/DeleteRepo.qml:70
msgid "Password Store removal failed !"
msgstr ""
#: ../qml/pages/settings/DeleteRepo.qml:79
msgid "Password Store deleted !"
msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:141
msgid "" msgid ""
"Importing a git repo will delete<br>any existing password store!" "Importing a git repo will delete<br>any existing password store!"
"<br>Continue ?" "<br>Continue ?"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:91 #: ../qml/pages/settings/ImportGitClone.qml:155
#: ../qml/pages/settings/ImportZip.qml:62 ../qml/pages/settings/InfoKeys.qml:77
msgid "Yes"
msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:104
msgid "An error occured during git clone !" msgid "An error occured during git clone !"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:113 #: ../qml/pages/settings/ImportGitClone.qml:164
#: ../qml/pages/settings/ImportZip.qml:84 #: ../qml/pages/settings/ImportZip.qml:88
msgid "Password store sucessfully imported !" msgid "Password store sucessfully imported !"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportGitClone.qml:125 #: ../qml/pages/settings/ImportGitClone.qml:176
msgid "Git Clone Import" msgid "Git Clone Import"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportKeyFile.qml:57 #: ../qml/pages/settings/ImportKeyFile.qml:7
msgid "Key import failed !"
msgstr ""
#: ../qml/pages/settings/ImportKeyFile.qml:66
msgid "Key successfully imported !"
msgstr ""
#: ../qml/pages/settings/ImportKeyFile.qml:77
msgid "GPG Key Import" msgid "GPG Key Import"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportZip.qml:61 #: ../qml/pages/settings/ImportKeyFile.qml:8
msgid "Key successfully imported !"
msgstr ""
#: ../qml/pages/settings/ImportKeyFile.qml:9
msgid "Key import failed !"
msgstr ""
#: ../qml/pages/settings/ImportKeyFile.qml:33
msgid "The file is not in a valid key format"
msgstr ""
#: ../qml/pages/settings/ImportSSHkey.qml:10
msgid "SSH Key Import"
msgstr ""
#: ../qml/pages/settings/ImportSSHkey.qml:11
msgid "SSH Key successfully imported !"
msgstr ""
#: ../qml/pages/settings/ImportSSHkey.qml:12
msgid "SSH Key import failed !"
msgstr ""
#: ../qml/pages/settings/ImportZip.qml:65
msgid "" msgid ""
"Importing a new zip will delete<br>any existing password store!<br>Continue ?" "Importing a new zip will delete<br>any existing password store!<br>Continue ?"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportZip.qml:75 #: ../qml/pages/settings/ImportZip.qml:79
msgid "Password store import failed !" msgid "Password store import failed !"
msgstr "" msgstr ""
#: ../qml/pages/settings/ImportZip.qml:96 #: ../qml/pages/settings/ImportZip.qml:100
msgid "Zip Password Store Import" msgid "Zip Password Store Import"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:41 #: ../qml/pages/settings/InfoKeys.qml:47
msgid "Key id : %1" msgid "No key found"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:54 #: ../qml/pages/settings/InfoKeys.qml:83
msgid "Key ID :"
msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:124
msgid "User IDs : "
msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:151
msgid "Delete this key" msgid "Delete this key"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:76 #: ../qml/pages/settings/InfoKeys.qml:173
msgid "You're are about to delete<br>%1<br>Continue ?" msgid "You're are about to delete<br>%1.<br>Continue ?"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:94 #: ../qml/pages/settings/InfoKeys.qml:187
msgid "Key removal failed !" msgid "Key removal failed !"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:103 #: ../qml/pages/settings/InfoKeys.qml:196
msgid "Key successfully deleted !" msgid "Key successfully deleted !"
msgstr "" msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:114 #: ../qml/pages/settings/InfoKeys.qml:208
msgid "An Error occured getting GPG keys !"
msgstr ""
#: ../qml/pages/settings/InfoKeys.qml:216
msgid "Info Keys" msgid "Info Keys"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:23 #: ../qml/pages/settings/Settings.qml:21
msgid "GPG" msgid "GPG"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:29 #: ../qml/pages/settings/Settings.qml:27
msgid "Import a GPG key file" msgid "Import a GPG key file"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:34 #: ../qml/pages/settings/Settings.qml:32
msgid "Show GPG keys" msgid "Show GPG keys"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:42 #: ../qml/pages/settings/Settings.qml:40
msgid "Password Store" msgid "Password Store"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:48 #: ../qml/pages/settings/Settings.qml:46
msgid "Import a Password Store using Git" msgid "Import a Password Store using Git"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:53 #: ../qml/pages/settings/Settings.qml:51
msgid "Import a Password Store Zip" msgid "Import a Password Store Zip"
msgstr "" msgstr ""
#: ../qml/pages/settings/Settings.qml:62 #: ../qml/pages/settings/Settings.qml:65
msgid "Warning: importing delete any exiting Password Store" msgid "Warning: importing delete any exiting Password Store"
msgstr "" msgstr ""

View File

@ -10,39 +10,43 @@ MainView {
id: root id: root
signal responsePassphraseDialog(bool canceled, string passphrase)
function initPass(rootView) { function initPass(rootView) {
Pass.init(rootView); Pass.initialize(rootView);
pageStack.push(Qt.resolvedUrl("pages/PasswordList.qml")); pageStack.push(Qt.resolvedUrl("pages/PasswordList.qml"));
} }
function callPassphraseDialog(useridHint, description, previousWasBad) { function callPassphraseDialog(useridHint, description, previousWasBad) {
//TODO use parameters to impove passphrase dialog //TODO use parameters to impove passphrase dialog
var passphraseDialog = PopupUtils.open(Qt.resolvedUrl("dialogs/PassphraseDialog.qml")); var pop = PopupUtils.open(passphraseDialog);
passphraseDialog.activateFocus(); pop.activateFocus();
var validated = function validated(passphrase) {
responsePassphraseDialog(false, passphrase);
};
var canceled = function canceled() {
responsePassphraseDialog(true, "");
};
passphraseDialog.validated.connect(validated);
passphraseDialog.canceled.connect(canceled);
} }
objectName: "mainView" objectName: "mainView"
applicationName: "utpass.qrouland" applicationName: "utpass.qrouland"
automaticOrientation: false automaticOrientation: true
width: units.gu(48) width: units.gu(45)
height: units.gu(80) height: units.gu(75)
PageStack { PageStack {
id: pageStack id: pageStack
anchors.fill: parent anchors.fill: parent
Component.onCompleted: { }
Component {
id: passphraseDialog
PassphraseDialog {
onValidated: {
console.info("valided");
Pass.responsePassphraseDialog(false, passphrase);
}
onCanceled: {
console.info("canceled");
Pass.responsePassphraseDialog(true, "");
}
} }
} }
} }

View File

@ -19,12 +19,14 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: theme.palette.normal.background
Text { Text {
text: copyText.text text: copyText.text
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: units.gu(2) anchors.leftMargin: units.gu(2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: theme.palette.normal.backgroundText
} }
Icon { Icon {

View File

@ -6,19 +6,33 @@ import Lomiri.Components.Themes 1.3
import Pass 1.0 import Pass 1.0
import QtQuick 2.4 import QtQuick 2.4
Component { Item {
//property string folder
id: fileDir
property string fName
property bool fIsDir
property bool commonBorder: true
property int lBorderwidth: 0
property int rBorderwidth: 0
property int tBorderwidth: 0
property int bBorderwidth: 0
property int commonBorderWidth: 0
property string borderColor: LomiriColors.warmGrey
signal clicked()
anchors.right: parent.right
anchors.left: parent.left
height: units.gu(5)
Rectangle { Rectangle {
id: fileDir anchors.fill: parent
property string activePasswordName
anchors.right: parent.right
anchors.left: parent.left
height: units.gu(5)
color: theme.palette.normal.background color: theme.palette.normal.background
Text { Text {
text: fileBaseName text: fileDir.fIsDir ? fileDir.fName : fileDir.fName.slice(0, -4) // remove .gpg if it's a file
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: units.gu(2) anchors.leftMargin: units.gu(2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -30,48 +44,36 @@ Component {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: units.gu(2) anchors.rightMargin: units.gu(2)
height: units.gu(4) height: units.gu(4)
name: fileIsDir ? "go-next" : "lock" name: fileDir.fIsDir ? "go-next" : "lock"
color: LomiriColors.orange color: LomiriColors.orange
} }
MouseArea { MouseArea {
// onClicked: {
// var path = fileDir.fdfolder + "/" + fileName;
// if (fileIsDir) {
// fileDir.fdfolder = path;
// //backAction.visible = true;
// // passwordListHeader.title = fileName;
// } else {
// console.debug("pass show %1".arg(path));
// Pass.show(path);
// }
// }
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: fileDir.clicked()
if (fileIsDir) {
folderModel.folder = folderModel.folder + "/" + fileName;
backAction.visible = true;
} else {
fileDir.activePasswordName = fileBaseName;
Pass.onDecrypted.connect(function(text) {
pageStack.push(Qt.resolvedUrl("../pages/Password.qml"), {
"plainText": text,
"title": fileDir.activePasswordName
});
});
Pass.onDecryptFailed.connect(function() {
PopupUtils.open(passwordPageDecryptError);
});
Pass.decrypt(folderModel.folder + "/" + fileName);
}
}
} }
CustomBorder { CustomBorder {
commonBorder: false id: cb
lBorderwidth: 0
rBorderwidth: 0
tBorderwidth: 0
bBorderwidth: 1
borderColor: LomiriColors.warmGrey
}
Component {
id: passwordPageDecryptError
ErrorDialog {
textError: i18n.tr("Decryption failed !")
}
commonBorder: fileDir.commonBorder
lBorderwidth: fileDir.lBorderwidth
rBorderwidth: fileDir.rBorderwidth
tBorderwidth: fileDir.tBorderwidth
bBorderwidth: fileDir.bBorderwidth
borderColor: fileDir.borderColor
} }
} }

View File

@ -0,0 +1,53 @@
import Git 1.0
import Lomiri.Components 1.3
import Pass 1.0
import QtQuick 2.4
Column {
signal repoUrlChanged(string url)
function setRepoUrl(url) {
repoUrlInput.text = url;
}
width: parent.width
spacing: units.gu(1)
Text {
id: repoUrlLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Repo Url')
color: theme.palette.normal.backgroundText
}
TextField {
id: repoUrlInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
placeholderText: "http(s)://<hostname>"
onTextChanged: repoUrlChanged(repoUrlInput.text)
}
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Button {
id: buttonClone
width: parent.width
color: theme.palette.normal.positive
text: i18n.tr('Clone')
onClicked: {
Git.cloneHttp(repoUrlInput.text, Pass.password_store);
}
}
}

View File

@ -0,0 +1,73 @@
import Git 1.0
import Lomiri.Components 1.3
import Pass 1.0
import QtQuick 2.4
Column {
signal repoUrlChanged(string url)
function setRepoUrl(url) {
repoUrlInput.text = url;
}
anchors.top: parent.fill
spacing: units.gu(1)
Text {
id: repoUrlLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Repo Url')
color: theme.palette.normal.backgroundText
}
TextField {
id: repoUrlInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
placeholderText: "http(s)://<username>@<hostname>"
onTextChanged: repoUrlChanged(repoUrlInput.text)
}
Text {
id: repoPasswordLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Password')
color: theme.palette.normal.backgroundText
}
TextField {
id: repoPasswordInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
echoMode: TextInput.Password
placeholderText: i18n.tr('Password')
}
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Button {
id: buttonClone
width: parent.width
color: theme.palette.normal.positive
text: i18n.tr('Clone')
onClicked: {
Git.cloneHttpPass(repoUrlInput.text, Pass.password_store, repoPasswordInput.text);
}
}
}

View File

@ -0,0 +1,149 @@
import Git 1.0
import Lomiri.Components 1.3
import Pass 1.0
import Utils 1.0
import QtQuick 2.4
Column {
property alias importSshPrivKeyButton : repoImportPrivKeyButton
property alias importSshPubKeyButton : repoImportPubKeyButton
property alias deleteSshPrivKeyButton : repoDeletePrivKeyButton
property alias deleteSshPubKeyButton : repoDeletePubKeyButton
property bool __sshPrivKeyAvailable : false
property bool __sshPubKeyAvailable : false
signal repoUrlChanged(string url)
function setRepoUrl(url) {
repoUrlInput.text = url;
}
function update() {
__sshPrivKeyAvailable = Utils.fileExists(Git.privKey);
__sshPubKeyAvailable = Utils.fileExists(Git.pubKey);
}
Component.onCompleted: {
update();
}
anchors.top: parent.fill
spacing: units.gu(1)
Text {
id: repoUrlLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Repo Url')
color: theme.palette.normal.backgroundText
}
TextField {
id: repoUrlInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
placeholderText: "<username>@<hostname>"
onTextChanged: repoUrlChanged(repoUrlInput.text)
}
Text {
id: repoPrivKeyLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('SSH private key')
color: theme.palette.normal.backgroundText
}
Button {
id: repoImportPrivKeyButton
width: parent.width
color: theme.palette.normal.positive
text: i18n.tr('Import SSH private key')
visible: !__sshPrivKeyAvailable
}
Button {
id: repoDeletePrivKeyButton
width: parent.width
color: theme.palette.normal.negative
text: i18n.tr('Delete SSH private key')
visible: __sshPrivKeyAvailable
}
Text {
id: repoPubKeyLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('SSH public key')
color: theme.palette.normal.backgroundText
}
Button {
id: repoDeletePubKeyButton
width: parent.width
color: theme.palette.normal.negative
text: i18n.tr('Delete SSH public key')
visible: __sshPrivKeyAvailable
}
Button {
id: repoImportPubKeyButton
width: parent.width
color: theme.palette.normal.positive
text: i18n.tr('Import SSH public key')
visible: !__sshPrivKeyAvailable
}
Text {
id: repoPassphraseLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Passphrase')
color: theme.palette.normal.backgroundText
}
TextField {
id: repoPassphraseInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
echoMode: TextInput.Password
placeholderText: i18n.tr('Passphrase')
}
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Button {
id: buttonClone
width: parent.width
color: theme.palette.normal.positive
text: i18n.tr('Clone')
onClicked: {
Git.cloneSshKey(repoUrlInput.text, Pass.password_store, repoPassphraseInput.text);
}
}
}

View File

@ -0,0 +1,73 @@
import "../dialogs"
import "../pages/headers"
import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import Lomiri.Content 1.3
import Pass 1.0
import QtQuick 2.4
import Utils 1.0
Page {
id: importKeyFilePage
property var activeTransfer
property alias contentPicker : contentPicker
property alias dialogImportKeyPageError : dialogImportKeyPageError
property alias dialogImportKeyPageSucess : dialogImportKeyPageSucess
property string headerTitle : i18n.tr("Import succeeded !")
property string dialogErrorTxt : i18n.tr("Import failed !")
property string dialogErrorDescriptionTxt : null
property string dialogSuccessTxt : i18n.tr("File Imported")
ContentPeerPicker {
id: contentPicker
anchors.top: importKeyHeader.bottom
anchors.bottom: parent.bottom
anchors.topMargin: importKeyFilePage.header.height
width: parent.width
visible: parent.visible
showTitle: false
contentType: ContentType.Text
handler: ContentHandler.Source
onCancelPressed: {
pageStack.pop();
}
}
ContentTransferHint {
id: transferHint
anchors.fill: parent
activeTransfer: importKeyFilePage.activeTransfer
}
Component {
id: dialogImportKeyPageError
ErrorDialog {
textError: importKeyFilePage.dialogErrorTxt
textErrorDescription: importKeyFilePage.dialogErrorDescriptionTxt
}
}
Component {
id: dialogImportKeyPageSucess
SuccessDialog {
textSuccess: importKeyFilePage.dialogSuccessTxt
onDialogClosed: {
pageStack.pop();
}
}
}
header: StackHeader {
id: importKeyHeader
title: importKeyFilePage.headerTitle
}
}

View File

@ -6,11 +6,12 @@ Dialog {
id: dialog id: dialog
property string textError property string textError
property string textErrorDescription : null
signal dialogClosed() signal dialogClosed()
title: i18n.tr("Error !") title: i18n.tr("Error !")
text: textError text: textErrorDescription ? (textError + "<br>" + textErrorDescription) : textError
Button { Button {
text: i18n.tr("Close") text: i18n.tr("Close")

View File

@ -5,18 +5,12 @@ import QtQuick 2.4
Dialog { Dialog {
id: dialog id: dialog
property string text
property string continueText: i18n.tr("Ok") property string continueText: i18n.tr("Ok")
property color continueColor: theme.palette.normal.positive property color continueColor: theme.palette.normal.positive
signal validated() signal validated()
signal canceled() signal canceled()
Text {
horizontalAlignment: Text.AlignHCenter
text: dialog.text
}
Button { Button {
id: continueButton id: continueButton

View File

@ -1,6 +1,7 @@
import "../components" import "../components"
import Lomiri.Components 1.3 import Lomiri.Components 1.3
import QtQuick 2.4 import QtQuick 2.4
import Utils 1.0
import "headers" import "headers"
Page { Page {
@ -8,7 +9,7 @@ Page {
Component.onCompleted: { Component.onCompleted: {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", "../../manifest_.json", false); xhr.open("GET", Utils.manifestPath(), false);
xhr.send(); xhr.send();
var mJson = JSON.parse(xhr.responseText); var mJson = JSON.parse(xhr.responseText);
manifestTitle.text = "<b>" + mJson.title + "</b>"; manifestTitle.text = "<b>" + mJson.title + "</b>";

View File

@ -26,6 +26,7 @@ Page {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
color: theme.palette.normal.background
Flow { Flow {
id: container id: container
@ -49,9 +50,9 @@ Page {
iconName: "back" iconName: "back"
text: "Back" text: "Back"
onTriggered: { onTriggered: {
passwordPage.plainText = ""; passwordPage.plainText = null;
for (var object in objects) { for (var object in objects) {
object.text = ""; object.text = null;
object.destroy(); object.destroy();
} }
pageStack.pop(); pageStack.pop();

View File

@ -1,5 +1,7 @@
import "../components" import "../components"
import "../dialogs"
import Lomiri.Components 1.3 import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import Pass 1.0 import Pass 1.0
import Qt.labs.folderlistmodel 2.1 import Qt.labs.folderlistmodel 2.1
import QtQuick 2.4 import QtQuick 2.4
@ -8,54 +10,206 @@ import "headers"
Page { Page {
id: passwordListPage id: passwordListPage
property string passwordStorePath property string __passwordStorePath
property var __passwords
property string __text_error_description: null
function __searchPasswords(filter) {
var ret = [];
if (__passwords) {
for (var i = 0; i < __passwords.length; i++) {
if (__passwords[i].toUpperCase().indexOf(filter.toUpperCase()) > -1)
ret.push(__passwords[i]);
}
}
return ret;
}
function __searchUpdateModel() {
const filter = passwordListHeader.searchBar.text;
console.info("filter : %1".arg(filter));
var ret = __searchPasswords(filter);
passwordListSearch.model.clear();
for (var i = 0; i < ret.length; i++) {
if (ret[i])
passwordListSearch.model.append({
"fileName": ret[i]
});
}
}
anchors.fill: parent anchors.fill: parent
Component.onCompleted: { Component.onCompleted: {
passwordStorePath = "file:" + Pass.password_store; passwordListPage.__passwordStorePath = "file:" + Pass.password_store;
Pass.onShowSucceed.connect(function(filename, text) {
pageStack.push(Qt.resolvedUrl("../pages/Password.qml"), {
"plainText": text,
"title": filename
});
});
Pass.onShowFailed.connect(function(code, message) {
switch (code) {
case 1: // UnexceptedError -> use the default (not translate) rnp error
__text_error_description = message;
break;
case 2: // BadPassphrase
__text_error_description = i18n.tr("Bad passphrase");
break;
case 3: // NoKeyFound
__text_error_description = i18n.tr("No valid key found");
break;
case 3: // DecryptFailed
__text_error_description = i18n.tr("Decryption failed");
break;
default:
console.warn("Unhandled error code");
__text_error_description = message;
break;
}
PopupUtils.open(passwordPageDecryptError);
});
Pass.onLsSucceed.connect(function(passwords) {
passwordListPage.__passwords = passwords;
__searchUpdateModel();
});
Pass.ls();
} }
Rectangle { Column {
id: passwordListEmpty
anchors.top: passwordListHeader.bottom anchors.top: passwordListHeader.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
visible: folderModel.count == 0 anchors.leftMargin: units.gu(2)
anchors.rightMargin: units.gu(2)
visible: passwordListNav.model.count === 0
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Text { Text {
text: i18n.tr("No password found<br>You can import a password store by cloning or importing a zip in the settings") text: i18n.tr("No password found")
anchors.horizontalCenter: parent.horizontalCenter width: parent.width
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
color: theme.palette.normal.backgroundText
}
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Text {
text: i18n.tr("You can import a password store by cloning or")
width: parent.width
horizontalAlignment: Text.AlignHCenter
color: theme.palette.normal.backgroundText
}
Text {
text: i18n.tr("importing a password store zip in the settings")
width: parent.width
horizontalAlignment: Text.AlignHCenter
color: theme.palette.normal.backgroundText
} }
} }
ListView { ListView {
id: passwordListNav
anchors.top: passwordListHeader.bottom anchors.top: passwordListHeader.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
spacing: 1 spacing: 1
visible: passwordListNav.model.count !== 0 && !passwordListHeader.searchBar.visible
model: FolderListModel { model: FolderListModel {
id: folderModel
nameFilters: ["*.gpg"] nameFilters: ["*.gpg"]
rootFolder: passwordStorePath rootFolder: passwordListPage.__passwordStorePath
folder: passwordStorePath folder: passwordListPage.__passwordStorePath
showDirs: true showDirs: true
} }
delegate: FileDir { delegate: Component {
id: fileDelegate FileDir {
fName: fileName
fIsDir: fileIsDir
onClicked: {
var path = passwordListNav.model.folder + "/" + fileName;
if (fileIsDir) {
passwordListNav.model.folder = path;
backAction.visible = true;
passwordListHeader.title = fileName;
} else {
console.debug("pass show %1".arg(path));
Pass.show(path);
}
}
}
} }
} }
ListView {
id: passwordListSearch
anchors.top: passwordListHeader.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
visible: passwordListNav.model.count !== 0 && passwordListHeader.searchBar.visible
model: ListModel {
}
delegate: Component {
FileDir {
fName: fileName
fIsDir: false
onClicked: {
var path = passwordListPage.__passwordStorePath + "/" + fileName;
console.debug("pass show %1".arg(path));
Pass.show(path);
}
}
}
}
Component {
id: passwordPageDecryptError
ErrorDialog {
textError: i18n.tr("Decryption failed !")
textErrorDescription: __text_error_description
}
}
Timer {
id: searchTimer
interval: 500
onTriggered: __searchUpdateModel()
}
header: MainHeader { header: MainHeader {
id: passwordListHeader id: passwordListHeader
searchBar.onTextChanged: searchTimer.restart()
leadingActionBar.height: units.gu(4) leadingActionBar.height: units.gu(4)
leadingActionBar.actions: [ leadingActionBar.actions: [
Action { Action {
@ -65,10 +219,14 @@ Page {
text: i18n.tr("Back") text: i18n.tr("Back")
visible: false visible: false
onTriggered: { onTriggered: {
folderModel.folder = folderModel.parentFolder; passwordListNav.model.folder = passwordListNav.model.parentFolder;
if (folderModel.rootFolder === folderModel.folder) console.debug(passwordListNav.model.folder);
if (passwordListNav.model.rootFolder === passwordListNav.model.folder) {
backAction.visible = false; backAction.visible = false;
passwordListHeader.title = i18n.tr("UTPass");
} else {
passwordListHeader.title = folderModel.folder;
}
} }
} }
] ]

View File

@ -2,25 +2,30 @@ import Lomiri.Components 1.3
import QtQuick 2.4 import QtQuick 2.4
PageHeader { PageHeader {
//property alias searchBarText: searchBar.text
//signal searchBarTextChanged(string text)
id: mainHeader id: mainHeader
property alias searchBar: searchBar
width: parent.width width: parent.width
height: units.gu(6) height: units.gu(6)
title: i18n.tr("UTPass") title: i18n.tr("UTPass")
trailingActionBar.height: units.gu(4) trailingActionBar.height: units.gu(4)
trailingActionBar.numberOfSlots: 2 trailingActionBar.numberOfSlots: 2
trailingActionBar.actions: [ trailingActionBar.actions: [
/*Action { TODO Action {
iconName: "search" iconName: !searchBar.visible ? "search" : "close"
text: i18n.tr("Search") text: i18n.tr("Search")
onTriggered: { onTriggered: {
searchBar.visible = !searchBar.visible searchBar.visible = !searchBar.visible;
labelTitle.visible = !searchBar.visible labelTitle.visible = !searchBar.visible;
if (searchBar.visible === true) { if (searchBar.visible === true)
searchBar.focus = true searchBar.focus = true;
}
} }
},*/ },
Action { Action {
iconName: "settings" iconName: "settings"
text: i18n.tr("Settings") text: i18n.tr("Settings")
@ -58,8 +63,6 @@ PageHeader {
height: units.gu(4) height: units.gu(4)
visible: false visible: false
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onFocusChanged: {
}
} }
} }

View File

@ -0,0 +1,96 @@
import "../../components"
import "../../dialogs"
import "../../settings"
import "../headers"
import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import Pass 1.0
import QtQuick 2.4
Page {
id: deleteRepoPage
Component.onCompleted: {
Pass.deletePasswordStoreSucceed.connect(function(keys_info) {
PopupUtils.open(deleteRepoPagePageDeleteSuccess);
});
Pass.deletePasswordStoreFailed.connect(function(message) {
PopupUtils.open(deleteRepoPagePageDeleteError);
});
}
Column {
id: deleteRepoPageListView
anchors.top: deleteRepoPageHeader.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
anchors.leftMargin: units.gu(2)
anchors.rightMargin: units.gu(2)
spacing: units.gu(1)
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Button {
id: buttonDeleteKey
width: parent.width
text: i18n.tr("Delete Password Store")
color: theme.palette.normal.negative
onClicked: {
PopupUtils.open(deleteRepoPagePageDeleteValidation, deleteRepoPage);
}
}
}
Component {
id: deleteRepoPagePageDeleteValidation
SimpleValidationDialog {
text: i18n.tr("You're are about to delete<br>the current Password Store.<br>Continue ?")
continueText: i18n.tr("Yes")
continueColor: theme.palette.normal.negative
onValidated: {
var status = Pass.deletePasswordStore();
}
}
}
Component {
id: deleteRepoPagePageDeleteError
ErrorDialog {
textError: i18n.tr("Password Store removal failed !")
}
}
Component {
id: deleteRepoPagePageDeleteSuccess
SuccessDialog {
textSuccess: i18n.tr("Password Store deleted !")
onDialogClosed: {
GitSettings.type = 0;
GitSettings.repoUrl = null;
pageStack.clear();
pageStack.push(Qt.resolvedUrl("../PasswordList.qml"));
}
}
}
header: StackHeader {
id: deleteRepoPageHeader
title: i18n.tr('Delete Password Store')
}
}

View File

@ -1,23 +1,67 @@
import "../../components" import "../../components"
import "../../dialogs" import "../../dialogs"
import "../../settings"
import "../headers" import "../headers"
import Git 1.0 import Git 1.0
import Lomiri.Components 1.3 import Lomiri.Components 1.3
import Lomiri.Components.Pickers 1.3
import Lomiri.Components.Popups 1.3 import Lomiri.Components.Popups 1.3
import Pass 1.0 import Pass 1.0
import QtQuick 2.4 import QtQuick 2.4
import Utils 1.0
Page { Page {
id: importGitClonePage id: importGitClonePage
property int __gitModeHTTP : 0
property int __gitModeHTTP_AUTH : 1
property int __gitModeSSH_KEY : 2
property string __repoUrl
function __loadForm() {
switch (combo.selectedIndex) {
case __gitModeHTTP:
importGitCloneForm.source = Qt.resolvedUrl("../../components/GitCloneHttp.qml");
break;
case __gitModeHTTP_AUTH:
importGitCloneForm.source = Qt.resolvedUrl("../../components/GitCloneHttpAuth.qml");
break;
case __gitModeSSH_KEY:
importGitCloneForm.source = Qt.resolvedUrl("../../components/GitCloneSshKey.qml");
break;
}
}
Component.onCompleted: { Component.onCompleted: {
Git.cloneSucceed.connect(function() {
GitSettings.type = combo.selectedIndex;
GitSettings.repoUrl = importGitClonePage.__repoUrl;
if(GitSettings.type != __gitModeSSH_KEY) { // ensure there no ssh key is kept if swicthing to another git mode
Utils.rmFile(Git.privKey);
Utils.rmFile(Git.pubKey);
}
PopupUtils.open(dialogGitCloneSuccess);
});
Git.cloneFailed.connect(function() {
PopupUtils.open(dialogGitCloneError);
});
if (GitSettings.repoUrl)
__repoUrl = GitSettings.repoUrl;
if (GitSettings.type < combo.count && GitSettings.type > 0)
combo.selectedIndex = GitSettings.type;
else
combo.selectedIndex = 0;
__loadForm();
PopupUtils.open(importGitCloneValidation, importGitClonePage); PopupUtils.open(importGitCloneValidation, importGitClonePage);
} }
Flow { Column {
anchors.top: importGitCloneHeader.bottom anchors.top: importGitCloneHeader.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.leftMargin: units.gu(2) anchors.leftMargin: units.gu(2)
anchors.rightMargin: units.gu(2) anchors.rightMargin: units.gu(2)
spacing: units.gu(1) spacing: units.gu(1)
@ -25,59 +69,66 @@ Page {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: units.gu(1) height: units.gu(1)
color: theme.palette.normal.background
} }
Text { OptionSelector {
id: repoUrlLabel id: combo
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width width: parent.width
text: i18n.tr('Repo Url') model: ["HTTP", "HTTP AUTH", "SSH KEY"]
onDelegateClicked: function(i) {
timer.setTimeout(function() {
__loadForm();
}, 500);
}
Timer {
id: timer
function setTimeout(cb, delayTime) {
timer.interval = delayTime;
timer.repeat = false;
timer.triggered.connect(cb);
timer.triggered.connect(function release() {
timer.triggered.disconnect(cb); // This is important
timer.triggered.disconnect(release); // This is important as well
});
timer.start();
}
}
} }
TextField { Loader {
id: repoUrlInput id: importGitCloneForm
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
}
Text {
id: repoPasswordLabel
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
text: i18n.tr('Password')
}
TextField {
id: repoPasswordInput
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width
echoMode: TextInput.Password
}
Button {
id: buttonAdd
width: parent.width width: parent.width
color: theme.palette.normal.positive onLoaded: {
text: i18n.tr('Clone') importGitCloneForm.item.repoUrlChanged.connect(function(url) {
onClicked: { importGitClonePage.__repoUrl = url;
var ret = false; });
if (repoPasswordInput.text === "") importGitCloneForm.item.setRepoUrl(importGitClonePage.__repoUrl);
ret = Git.clone_http(repoUrlInput.text, Pass.password_store);
else switch (combo.selectedIndex) {
ret = Git.clone_http_pass(repoUrlInput.text, Pass.password_store, repoPasswordInput.text); case __gitModeHTTP:
if (ret) break;
PopupUtils.open(dialogImportGitCloneSuccess); case __gitModeHTTP_AUTH:
else break;
PopupUtils.open(importGitCloneError, importGitClonePage); case __gitModeSSH_KEY:
importGitCloneForm.item.importSshPrivKeyButton.clicked.connect(function() {
pageStack.push(Qt.resolvedUrl("ImportSSHkey.qml"), {
"isPrivateKey": true
});
});
importGitCloneForm.item.importSshPubKeyButton.clicked.connect(function() {
pageStack.push(Qt.resolvedUrl("ImportSSHkey.qml"), {
"isPrivateKey": false
});
});
break;
}
} }
} }
@ -98,7 +149,7 @@ Page {
} }
Component { Component {
id: importGitCloneError id: dialogGitCloneError
ErrorDialog { ErrorDialog {
textError: i18n.tr("An error occured during git clone !") textError: i18n.tr("An error occured during git clone !")
@ -107,13 +158,13 @@ Page {
} }
Component { Component {
id: dialogImportGitCloneSuccess id: dialogGitCloneSuccess
SuccessDialog { SuccessDialog {
textSuccess: i18n.tr("Password store sucessfully imported !") textSuccess: i18n.tr("Password store sucessfully imported !")
onDialogClosed: { onDialogClosed: {
pageStack.pop(); pageStack.clear();
pageStack.pop(); pageStack.push(Qt.resolvedUrl("../PasswordList.qml"));
} }
} }

View File

@ -1,80 +1,46 @@
import "../../dialogs" import "../../components"
import "../headers"
import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import Lomiri.Content 1.3
import Pass 1.0 import Pass 1.0
import QtQuick 2.4
import Utils 1.0
Page { ImportFile {
id: importKeyFilePage id: importKeyFilePage
property var activeTransfer headerTitle: i18n.tr("GPG Key Import")
dialogSuccessTxt : i18n.tr("Key successfully imported !")
dialogErrorTxt : i18n.tr("Key import failed !")
ContentPeerPicker { contentPicker.onPeerSelected: {
anchors.top: importKeyHeader.bottom {
anchors.bottom: parent.bottom peer.selectionType = ContentTransfer.Single;
anchors.topMargin: importKeyFilePage.header.height importKeyFilePage.activeTransfer = peer.request();
width: parent.width importKeyFilePage.activeTransfer.stateChanged.connect(function() {
visible: parent.visible if (importKeyFilePage.activeTransfer.state === ContentTransfer.Charged) {
showTitle: false console.log("Charged");
contentType: ContentType.Text console.log(importKeyFilePage.activeTransfer.items[0].url);
handler: ContentHandler.Source Pass.importGPGKey(importKeyFilePage.activeTransfer.items[0].url);
onPeerSelected: { Pass.importGPGKeySucceed.connect(function() {
peer.selectionType = ContentTransfer.Single; Utils.rmFile(importKeyFilePage.activeTransfer.items[0].url);
importKeyFilePage.activeTransfer = peer.request(); importKeyFilePage.activeTransfer = null;
importKeyFilePage.activeTransfer.stateChanged.connect(function() { PopupUtils.open(importKeyFilePage.dialogImportKeyPageSucess);
if (importKeyFilePage.activeTransfer.state === ContentTransfer.Charged) { });
console.log("Charged"); Pass.importGPGKeyFailed.connect(function(err, message) {
console.log(importKeyFilePage.activeTransfer.items[0].url); Utils.rmFile(importKeyFilePage.activeTransfer.items[0].url);
var status = Pass.gpgImportKeyFromFile(importKeyFilePage.activeTransfer.items[0].url); importKeyFilePage.activeTransfer = null;
Utils.rmFile(importKeyFilePage.activeTransfer.items[0].url); switch (code) {
if (status) case 1: // UnexceptedError -> use the default (not translate) rnp error
PopupUtils.open(dialogImportKeyPageSucess); __text_error_description = message;
else break;
PopupUtils.open(dialogImportKeyPageError); case 2: // BadFormat
importKeyFilePage.activeTransfer = null; __text_error_description = i18n.tr("The file is not in a valid key format");
} break;
}); default:
} console.warn("Unhandled error code");
onCancelPressed: { __text_error_description = message;
pageStack.pop(); break;
} }
PopupUtils.open(importKeyFilePage.dialogImportKeyPageError);
});
}
});
}
} }
ContentTransferHint {
id: transferHint
anchors.fill: parent
activeTransfer: importKeyFilePage.activeTransfer
}
Component {
id: dialogImportKeyPageError
ErrorDialog {
textError: i18n.tr("Key import failed !")
}
}
Component {
id: dialogImportKeyPageSucess
SuccessDialog {
textSuccess: i18n.tr("Key successfully imported !")
onDialogClosed: {
pageStack.pop();
}
}
}
header: StackHeader {
id: importKeyHeader
title: i18n.tr("GPG Key Import")
}
} }

View File

@ -0,0 +1,34 @@
import "../../components"
import Utils 1.0
import Git 1.0
ImportFile {
id: importSSHKeyPage
property bool isPrivateKey
headerTitle: i18n.tr("SSH Key Import")
dialogSuccessTxt : i18n.tr("SSH Key successfully imported !")
dialogErrorTxt : i18n.tr("SSH Key import failed !")
contentPicker.onPeerSelected: {
{
peer.selectionType = ContentTransfer.Single;
importSSHKeyPage.activeTransfer = peer.request();
importSSHKeyPage.activeTransfer.stateChanged.connect(function() {
if (importSSHKeyPage.activeTransfer.state === ContentTransfer.Charged) {
console.log("Charged");
console.log(importSSHKeyPage.activeTransfer.items[0].url);
var ret = Git.importSshKey(importSSHKeyPage.activeTransfer.items[0].url, isPrivateKey);
Utils.rmFile(importSSHKeyPage.activeTransfer.items[0].url);
importSSHKeyPage.activeTransfer = null;
if(ret) {
PopupUtils.open(importSSHKeyPage.dialogImportKeyPageSucess);
} else {
PopupUtils.open(importSSHKeyPage.dialogImportKeyPageError);
}
}
});
}
}
}

View File

@ -32,13 +32,17 @@ Page {
if (importZipPage.activeTransfer.state === ContentTransfer.Charged) { if (importZipPage.activeTransfer.state === ContentTransfer.Charged) {
console.log("Charged"); console.log("Charged");
console.log(importZipPage.activeTransfer.items[0].url); console.log(importZipPage.activeTransfer.items[0].url);
var status = Utils.unzip(importZipPage.activeTransfer.items[0].url, Pass.getPasswordStore()); var status = Utils.unzip(importZipPage.activeTransfer.items[0].url, Pass.password_store);
Utils.rmFile(importZipPage.activeTransfer.items[0].url); Utils.unzipSucceed.connect(function() {
if (status) Utils.rmFile(importZipPage.activeTransfer.items[0].url);
importZipPage.activeTransfer = null;
PopupUtils.open(dialogImportZipPageSuccess); PopupUtils.open(dialogImportZipPageSuccess);
else });
Utils.unzipFailed.connect(function() {
Utils.rmFile(importZipPage.activeTransfer.items[0].url);
importZipPage.activeTransfer = null;
PopupUtils.open(dialogImportZipPageError); PopupUtils.open(dialogImportZipPageError);
importZipPage.activeTransfer = null; });
} }
}); });
} }
@ -83,8 +87,8 @@ Page {
SuccessDialog { SuccessDialog {
textSuccess: i18n.tr("Password store sucessfully imported !") textSuccess: i18n.tr("Password store sucessfully imported !")
onDialogClosed: { onDialogClosed: {
pageStack.pop(); pageStack.clear();
pageStack.pop(); pageStack.push(Qt.resolvedUrl("../PasswordList.qml"));
} }
} }

View File

@ -9,7 +9,48 @@ import QtQuick 2.4
Page { Page {
id: infoKeysPage id: infoKeysPage
property string currentKey property list<QtObject> __keys
property QtObject __currentKey
Component.onCompleted: {
Pass.getAllGPGKeysSucceed.connect(function(keys_info) {
infoKeysPage.__keys = keys_info.keys;
});
Pass.getAllGPGKeysFailed.connect(function(message) {
PopupUtils.open(infoKeysPageGetAllError);
});
Pass.deleteGPGKeySucceed.connect(function(keys_info) {
PopupUtils.open(infoKeysPageDeleteSuccess);
});
Pass.deleteGPGKeyFailed.connect(function(message) {
PopupUtils.open(infoKeysPageDeleteError);
});
Pass.getAllGPGKeys();
}
Column {
anchors.top: infoKeysHeader.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
anchors.leftMargin: units.gu(2)
anchors.rightMargin: units.gu(2)
visible: infoKeysPage.__keys.length === 0
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
Text {
text: i18n.tr("No key found")
width: parent.width
horizontalAlignment: Text.AlignHCenter
color: theme.palette.normal.backgroundText
}
}
ListView { ListView {
id: infoKeysListView id: infoKeysListView
@ -18,7 +59,10 @@ Page {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.left: parent.left anchors.left: parent.left
model: Pass.gpgGetAllKeysModel() anchors.leftMargin: units.gu(2)
anchors.rightMargin: units.gu(2)
visible: infoKeysPage.__keys.length !== 0
model: infoKeysPage.__keys
delegate: Grid { delegate: Grid {
columns: 1 columns: 1
@ -33,15 +77,67 @@ Page {
} }
Text { Text {
id: uidKey
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: i18n.tr('Key id : %1').arg(model.modelData.uid) text: i18n.tr('Key ID :')
color: theme.palette.normal.backgroundText color: theme.palette.normal.backgroundText
} }
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: {
if (!model.modelData)
"";
else
model.modelData.keyid;
}
color: theme.palette.normal.backgroundText
}
Rectangle {
width: parent.width
height: units.gu(1)
color: theme.palette.normal.background
}
ListModel {
id: userIdsModel
Component.onCompleted: {
if (model.modelData) {
for (var i = 0; i < model.modelData.userids.length; ++i) {
userIdsModel.append({
"model": model.modelData.userids[i]
});
}
}
}
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: i18n.tr('User IDs : ')
color: theme.palette.normal.backgroundText
}
Repeater {
model: userIdsModel
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: modelData
color: theme.palette.normal.backgroundText
}
}
Rectangle { Rectangle {
width: parent.width width: parent.width
height: units.gu(1) height: units.gu(1)
@ -51,10 +147,11 @@ Page {
Button { Button {
id: buttonDeleteKey id: buttonDeleteKey
width: parent.width
text: i18n.tr("Delete this key") text: i18n.tr("Delete this key")
color: theme.palette.normal.negative color: theme.palette.normal.negative
onClicked: { onClicked: {
infoKeysPage.currentKey = model.modelData.uid; infoKeysPage.__currentKey = model.modelData;
PopupUtils.open(infoKeysPageDeleteValidation, infoKeysPage); PopupUtils.open(infoKeysPageDeleteValidation, infoKeysPage);
} }
} }
@ -73,15 +170,11 @@ Page {
id: infoKeysPageDeleteValidation id: infoKeysPageDeleteValidation
SimpleValidationDialog { SimpleValidationDialog {
text: i18n.tr("You're are about to delete<br>%1<br>Continue ?").arg(infoKeysPage.currentKey) text: i18n.tr("You're are about to delete<br>%1.<br>Continue ?").arg(infoKeysPage.__currentKey.keyid)
continueText: i18n.tr("Yes") continueText: i18n.tr("Yes")
continueColor: theme.palette.normal.negative continueColor: theme.palette.normal.negative
onValidated: { onValidated: {
var status = Pass.gpgDeleteKeyId(infoKeysPage.currentKey); var status = Pass.deleteGPGKey(infoKeysPage.__currentKey);
if (status)
PopupUtils.open(infoKeysPageDeleteSuccess);
else
PopupUtils.open(infoKeysPageDeleteError);
} }
} }
@ -102,12 +195,21 @@ Page {
SuccessDialog { SuccessDialog {
textSuccess: i18n.tr("Key successfully deleted !") textSuccess: i18n.tr("Key successfully deleted !")
onDialogClosed: { onDialogClosed: {
infoKeysListView.model = Pass.gpgGetAllKeysModel(); infoKeysListView.model = Pass.getAllGPGKeys();
} }
} }
} }
Component {
id: infoKeysPageGetAllError
ErrorDialog {
textError: i18n.tr("An Error occured getting GPG keys !")
}
}
header: StackHeader { header: StackHeader {
id: infoKeysHeader id: infoKeysHeader

View File

@ -7,8 +7,6 @@ import QtQuick 2.4
Page { Page {
id: settingsPage id: settingsPage
property string gpgKeyId: ""
Flow { Flow {
anchors.top: settingsHeader.bottom anchors.top: settingsHeader.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@ -53,12 +51,17 @@ Page {
text: i18n.tr('Import a Password Store Zip') text: i18n.tr('Import a Password Store Zip')
} }
PageStackLink {
page: Qt.resolvedUrl("DeleteRepo.qml")
text: i18n.tr('Delete Password Store')
}
Text { Text {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
width: parent.width width: parent.width
height: units.gu(4) height: units.gu(4)
color: LomiriColors.red color: theme.palette.normal.negative
text: i18n.tr('Warning: importing delete any exiting Password Store') text: i18n.tr('Warning: importing delete any exiting Password Store')
} }

View File

@ -0,0 +1,7 @@
import Qt.labs.settings 1.0
pragma Singleton
Settings {
property int type: 0
property string repoUrl: null
}

2
qml/settings/qmldir Normal file
View File

@ -0,0 +1,2 @@
module settings
singleton GitSettings 1.0 GitSettings.qml

20
tests.in.cpp Normal file
View File

@ -0,0 +1,20 @@
#include <QGuiApplication>
#include <QCoreApplication>
#include <QUrl>
#include <QString>
#include <QQuickView>
#include <QtQml>
#include <QtQuickTest/quicktest.h>
int main(int argc, char *argv[])
{
qDebug() << "Starting app from tests.cpp";
new QGuiApplication(argc, argv);
QGuiApplication::setApplicationName("utpass.qrouland");
return quick_test_main(argc, argv, "@TESTS_PATH@", "@TESTS_PATH@");
}

View File

@ -0,0 +1 @@
SomePassword

View File

@ -0,0 +1 @@
<EFBFBD>^<03><><EFBFBD><EFBFBD><16>8m@<40><>z<>,fL<66><><D58E>j<EFBFBD><1C><>'<12><>!<21>[Pfi0<69>p<EFBFBD><70>:<3A><02>0C<30>9e;<13><><EFBFBD>/<2F><><EFBFBD><04>c<EFBFBD>;Y<>\<5C><><EFBFBD><EFBFBD><EFBFBD>L<EFBFBD>Gw<>H<EFBFBD><48>` <10>Pt.E8<45><38><EFBFBD>y<EFBFBD><79>[z[m<><6D><EFBFBD><EFBFBD><EFBFBD> <20><03>O<EFBFBD><4F><EFBFBD>XkL<6B><4C>c<EFBFBD><63><EFBFBD><1C>pC<70>+<2B><>$<24><>I<EFBFBD>I{<7B>H<EFBFBD>v<EFBFBD><76><EFBFBD>0<10>]<5D>r<EFBFBD><72>H<EFBFBD><1A><08>l/<2F>!l<><03><><EFBFBD><EFBFBD><EFBFBD>0

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -23,6 +23,7 @@ add_library(${PLUGIN} MODULE ${SRC})
set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN}) set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN})
qt5_use_modules(${PLUGIN} Qml Quick DBus) qt5_use_modules(${PLUGIN} Qml Quick DBus)
set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}")
install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/) install(TARGETS ${PLUGIN} DESTINATION ${QT_IMPORTS_DIR}/${PLUGIN}/)

View File

@ -0,0 +1,41 @@
#ifndef UTPASSPHRASEPROVIDER_H
#define UTPASSPHRASEPROVIDER_H
#include <QObject>
#include <gpg-error.h>
// extern "C" {
// #include <rnp/rnp.h>
// }
class TesTPassphraseProvider : public QObject
{
Q_OBJECT
private:
explicit TesTPassphraseProvider(QObject * parent = nullptr)
{}
public:
~TesTPassphraseProvider() = default;
static TesTPassphraseProvider &instance()
{
static TesTPassphraseProvider instance;
return instance;
}
TesTPassphraseProvider(TesTPassphraseProvider const &) = delete;
void operator=(TesTPassphraseProvider const &) = delete;
static bool
example_pass_provider(rnp_ffi_t ffi,
void *app_ctx,
rnp_key_handle_t key,
const char *pgp_context,
char buf[],
size_t buf_len)
{
strncpy(buf, "utpasspassphrase", buf_len);
return true;
}
};
#endif

View File

@ -6,5 +6,5 @@
void TestsUtilsPlugin::registerTypes(const char *uri) void TestsUtilsPlugin::registerTypes(const char *uri)
{ {
//@uri TestUtils //@uri TestUtils
qmlRegisterSingletonType<TestsUtilsPlugin>(uri, 1, 0, "TestUtils", [](QQmlEngine *, QJSEngine *) -> QObject * { return new TestsUtils; }); qmlRegisterSingletonType<TestsUtilsPlugin>(uri, 1, 0, "TestsUtils", [](QQmlEngine *, QJSEngine *) -> QObject * { return new TestsUtils; });
} }

View File

@ -1,2 +1,2 @@
module TestUtils module TestsUtils
plugin TestUtils plugin TestsUtils

View File

@ -3,17 +3,21 @@
#include <QUrl> #include <QUrl>
#include <QUuid> #include <QUuid>
#include <QtCore/QStandardPaths> #include <QtCore/QStandardPaths>
#include <memory>
#include <quazip5/JlCompress.h> #include <quazip5/JlCompress.h>
//#include "passphraseprovider.h"
#include "utils.h" #include "utils.h"
TestsUtils::TestsUtils()
//m_passphrase_povider(std::unique_ptr<TesTPassphraseProvider>(new TesTPassphraseProvider()))
{}
QString TestsUtils::getTempPath() QString TestsUtils::getTempPath()
{ {
qFatal("yp");
// Get the system's temporary directory // Get the system's temporary directory
QString tempDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
qDebug() << "TempDir : " << tempDir;
// Generate a unique UUID // Generate a unique UUID
QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
@ -22,17 +26,51 @@ QString TestsUtils::getTempPath()
QString newTempDir = tempDir + "/" + uuid; QString newTempDir = tempDir + "/" + uuid;
QDir dir; QDir dir;
if (!dir.exists(newTempDir)) { dir.mkpath(newTempDir);
// Create the directory
if (dir.mkpath(newTempDir)) { qDebug() << "[TestUtils] TempDir : " << newTempDir;
return newTempDir; // Return the path if successful return newTempDir;
} else { }
return "Failed to create directory"; // Return an error message
} bool TestsUtils::fileExists(QUrl path)
} else { {
return newTempDir; // If the directory already exists, return its path QString p = path.toLocalFile();
auto ret = QFileInfo::exists(p) && QFileInfo(p).isFile();
qDebug() << "[TestUtils]" << p << "is existing file :" << ret;
return ret;
}
void TestsUtils::copyFolder(QUrl sourceFolderUrl, QUrl destFolderUrl)
{
auto sourceFolder = sourceFolderUrl.toLocalFile();
auto destFolder = destFolderUrl.toLocalFile();
QDir sourceDir(sourceFolder);
if (!sourceDir.exists())
return;
QDir destDir(destFolder);
if (!destDir.exists()) {
destDir.mkdir(destFolder);
}
qDebug() << "[TestUtils]" << "Copy files from" << sourceFolder << "to" << destFolder;
QStringList files = sourceDir.entryList(QDir::Files);
for (int i = 0; i < files.count(); i++) {
QString srcName = sourceFolder + "/" + files[i];
QString destName = destFolder + "/" + files[i];
QFile::copy(srcName, destName);
qDebug() << "[TestUtils]" << "Copy file from" << srcName << "to" << destName;
}
files.clear();
files = sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
for (int i = 0; i < files.count(); i++) {
QString srcName = sourceFolder + "/" + files[i];
QString destName = destFolder + "/" + files[i];
this->copyFolder(srcName, destName);
} }
} }
// QObject *TestsUtils::getTestPassphraseProvider()
// {
// return &TesTPassphraseProvider::instance();
// }

View File

@ -1,19 +1,25 @@
#ifndef TESTSUTILS_H #ifndef TESTSUTILS_H
#define TESTSUTILS_H #define TESTSUTILS_H
//#include "passphraseprovider.h"
#include <QObject> #include <QObject>
#include <QUrl> #include <QUrl>
#include <QQuickWindow> #include <QQuickWindow>
#include <memory>
class TestsUtils : public QObject class TestsUtils : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
TestsUtils() = default; TestsUtils();
~TestsUtils() override = default; ~TestsUtils() override = default;
Q_INVOKABLE QString getTempPath(); Q_INVOKABLE QString getTempPath();
Q_INVOKABLE bool fileExists(QUrl path);
Q_INVOKABLE void copyFolder(QUrl sourceFolder, QUrl destFolder);
//Q_INVOKABLE QObject *getTestPassphraseProvider();
}; };
#endif #endif

View File

@ -1,11 +0,0 @@
import Git 1.0
import QtQuick 2.9
import QtTest 1.2
TestCase {
function test_git_clone() {
verify(Git.clone("", ""));
}
name: "git"
}

View File

@ -1,11 +0,0 @@
import Git 1.0
import QtQuick 2.9
import QtTest 1.2
TestCase {
function test_git_clone() {
verify(Git.clone("", ""));
}
name: "git"
}

View File

@ -0,0 +1,20 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
import TestsUtils 1.0
TestCase {
//Pass.passphrase_provider = TestsUtils.getTestPassphraseProvider();
property string password_store
property string gpg_home
function init() {
Pass.initialize(null);
gpg_home = TestsUtils.getTempPath();
Pass.gpg_home = gpg_home;
password_store = TestsUtils.getTempPath();
Pass.password_store = password_store;
}
}

View File

@ -0,0 +1,63 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
import TestsUtils 1.0
PassTestCase {
//TODO some additionanl error test
function init_data() {
return [{
"spy": getAllGPGKeysSucceed,
"err_msg": null,
"add_home_gpg_data": false,
"keys": []
}, {
"spy": getAllGPGKeysSucceed,
"err_msg": null,
"add_home_gpg_data": true,
"keys": [{
"fingerprint": "F97476B6FA58A84B004E4616D4BAF1FDB7BA9ECC",
"keyid": "D4BAF1FDB7BA9ECC",
"userids": "UTPass Test <utpass@test.org>",
"hasSecret": true
}]
}];
}
function test_get_keys(data) {
if (data.add_home_gpg_data === true)
TestsUtils.copyFolder(Qt.resolvedUrl("../../assets/gpghome"), Qt.resolvedUrl(gpg_home));
var keys;
Pass.getAllGPGKeysSucceed.connect(function(keys_info) {
keys = keys_info;
});
Pass.getAllGPGKeys();
data.spy.wait();
verify(keys.length === data.keys.length, "Nb keys %1 but was excepted %2".arg(keys.length).arg(data.nb_keys));
for (var i = 0; i < keys.length; i++) {
console.info(keys.keys[i]);
console.info(keys.keys[i].keyid);
verify(keys.keys[i].fingerprint === data.keys[i].fingerprint, "fingerprint is %1 but was excepted %2".arg(keys.keys[i].fingerprint).arg(data.keys[i].fingerprint));
verify(keys.keys[i].keyid === data.keys[i].keyid, "keyid is %1 but was excepted %2".arg(keys.keys[i].keyid).arg(data.keys[i].keyid));
verify(keys.keys[i].userids[0] === data.keys[i].userids, "userids is %1 but was excepted %2".arg(keys.keys[i].userids[0]).arg(data.keys[i].userids));
verify(keys.keys[i].hasSecret === data.keys[i].hasSecret, "hasSecret is %1 but was excepted %2".arg(keys.keys[i].hasSecret).arg(data.keys[i].hasSecret));
}
}
SignalSpy {
id: getAllGPGKeysSucceed
target: Pass
signalName: "getAllGPGKeysSucceed"
}
SignalSpy {
id: getAllGPGKeysFailed
target: Pass
signalName: "getAllGPGKeysFailed"
}
}

View File

@ -0,0 +1,53 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
import TestsUtils 1.0
PassTestCase {
function init_data() {
return [{
"file": Qt.resolvedUrl("../../assets/gpg/test_key.gpg"),
"spy": importGPGKeySucceed,
"err_msg": null
}, {
"file": Qt.resolvedUrl("../../assets/gpg/test_key_do_not_exist.gpg"),
"spy": importGPGKeyFailed,
"err_msg": "Error reading file"
}, {
"file": Qt.resolvedUrl("../../assets/gpg/test_key_invalid.gpg"),
"spy": importGPGKeyFailed,
"err_msg": "Bad state"
}];
}
function test_import_key(data) {
var err_msg;
Pass.importGPGKeyFailed.connect(function(message) {
err_msg = message;
});
Pass.importGPGKey(data.file);
data.spy.wait();
if (data.err_msg) {
verify(err_msg === data.err_msg, "Should return %1 but return %2".arg(data.err_msg).arg(err_msg));
} else {
console.info(Qt.resolvedUrl("%1/pubkeyring.pgp".arg(gpg_home)));
verify(TestsUtils.fileExists(Qt.resolvedUrl("%1/pubring.pgp".arg(gpg_home))), "%1/pubring.pgp should be created".arg(gpg_home));
verify(TestsUtils.fileExists(Qt.resolvedUrl("%1/secring.pgp".arg(gpg_home))), "%1/secring.pgp should be created".arg(gpg_home));
}
}
SignalSpy {
id: importGPGKeySucceed
target: Pass
signalName: "importGPGKeySucceed"
}
SignalSpy {
id: importGPGKeyFailed
target: Pass
signalName: "importGPGKeyFailed"
}
}

View File

@ -0,0 +1,44 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
import TestsUtils 1.0
PassTestCase {
//TODO some additionanl error test
function init_data() {
return [{
"spy": lsSucceed,
"add_home_gpg_data": true,
"passwords": ["test.gpg"]
}, {
"spy": lsSucceed,
"add_home_gpg_data": false,
"passwords": []
}];
}
function test_ls(data) {
if (data.add_home_gpg_data === true)
TestsUtils.copyFolder(Qt.resolvedUrl("../../assets/password-store"), Qt.resolvedUrl(password_store));
var passwords;
Pass.lsSucceed.connect(function(ret) {
passwords = ret;
});
Pass.ls();
data.spy.wait();
verify(passwords.length === data.passwords.length, "Should return %1 password(s) but return %2 password(s)".arg(data.nb_password).arg(passwords.length));
for (var i = 0; data.passwords.length; i++) {
verify(passwords[i] === data.passwords[i], "%1 name should be %2 but is %3".arg(i).arg(data.passwords[i]).arg(passwords[i]));
}
}
SignalSpy {
id: lsSucceed
target: Pass
signalName: "lsSucceed"
}
}

View File

@ -0,0 +1,58 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
import TestsUtils 1.0
PassTestCase {
// TODO This test need to fixed by providing custom stub password provider to the pass plugin for succeed tests
//TODO some additionanl error test
function init_data() {
return [{
"spy": showFailed,
"err_msg": "Bad password",
"add_password_store_data": true,
"file": "../../assets/gpg/clear_text.txt.gpg"
}, {
"spy": showFailed,
"err_msg": "No suitable key",
"add_password_store_data": false,
"file": "../../assets/gpg/clear_text.txt.gpg"
}];
}
function test_pass_show(data) {
if (data.add_password_store_data === true)
TestsUtils.copyFolder(Qt.resolvedUrl("../../assets/gpghome"), Qt.resolvedUrl(gpg_home));
var fname, ctext;
Pass.showSucceed.connect(function(file_name, clear_text) {
fname = file_name;
ctext = clear_text;
});
var err_msg;
Pass.showFailed.connect(function(err) {
err_msg = err;
});
Pass.show(Qt.resolvedUrl(data.file));
data.spy.wait();
if (data.err_msg)
verify(err_msg === data.err_msg, "Should return %1 but return %2".arg(data.err_msg).arg(err_msg));
else
verify(false);
}
SignalSpy {
id: showSucceed
target: Pass
signalName: "showSucceed"
}
SignalSpy {
id: showFailed
target: Pass
signalName: "showFailed"
}
}

13
tests/units/tst_git.qml Normal file
View File

@ -0,0 +1,13 @@
import Pass 1.0
import QtQuick 2.9
import QtTest 1.2
TestCase {
function test_import_key() {
var homedir = TestUtils.getTempPath();
Pass;
verify(false);
}
name: "git"
}

View File

@ -1,6 +1,6 @@
import QtQuick 2.9 import QtQuick 2.9
import QtTest 1.2 import QtTest 1.2
import TestUtils 1.0 import TestsUtils 1.0
import Utils 1.0 import Utils 1.0
TestCase { TestCase {