kutils: new kpasswdstore library

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
This commit is contained in:
Ivailo Monev 2022-04-04 19:44:39 +03:00
parent b2155a46b1
commit c19956a9ca
10 changed files with 518 additions and 4 deletions

View file

@ -206,7 +206,7 @@ set_package_properties(LibCDIO PROPERTIES
TYPE OPTIONAL
)
find_package(Exiv2 0.21)
macro_optional_find_package(Exiv2 0.21)
set_package_properties(Exiv2 PROPERTIES
DESCRIPTION "Image metadata library and tools"
URL "http://www.exiv2.org"
@ -214,6 +214,14 @@ set_package_properties(Exiv2 PROPERTIES
PURPOSE "Exiv2 metadata extraction and image rotation based on the data"
)
macro_optional_find_package(OpenSSL)
set_package_properties(OpenSSL PROPERTIES
DESCRIPTION "Robust, commercial-grade, full-featured toolkit for general-purpose cryptography and secure communication"
URL "https://www.openssl.org/"
TYPE RECOMMENDED
PURPOSE "Store password securely"
)
################# configure checks and create the configured files #################
include(ConfigureChecks.cmake)

View file

@ -22,8 +22,8 @@ build_script:
libmagick++-dev libmpv-dev xorg-dev mesa-common-dev \
libavahi-common-dev libwebp-dev libudev-dev liblzma-dev \
libexiv2-dev libbz2-dev libacl1-dev libcdio-dev libextractor-dev \
libcurl4-openssl-dev libdbusmenu-katie media-player-info \
shared-mime-info media-player-info xdg-utils ccache
libcurl4-openssl-dev libssl-dev libdbusmenu-katie \
media-player-info shared-mime-info media-player-info xdg-utils ccache
export PATH="/usr/lib/ccache/:$PATH"

View file

@ -59,6 +59,7 @@
# KDE4_PLASMA_LIBS - the plasma library and all depending librairies
# KDE4_KEXIV2_LIBS - the kexiv2 library and all depending libraries
# KDE4_KMEDIAPLAYER_LIBS - the kmediaplayer library and all depending libraries
# KDE4_KPASSWDSTORE_LIBS - the kpasswdstore library and all depending libraries
#
# The variable INSTALL_TARGETS_DEFAULT_ARGS can be used when installing libraries
# or executables into the default locations.
@ -288,6 +289,7 @@ set(_kde_libraries
kdeui
kdnssd
kexiv2
kpasswdstore
kfile
kidletime
kio

View file

@ -189,12 +189,15 @@
13050 Kate (Scripting)
13060 Kate (Indentation)
#kfilereplace
# kfilereplace
23000 KFileReplace (kfilereplacepart)
# KEXIV2 - KDE C++ wrapper for LibExiv2
51003 KEXIV2
# kpasswdstore
51004 kpasswdstore
# kdemultimedia
67100 kmix

View file

@ -9,6 +9,7 @@ include_directories(
add_subdirectory(kmediaplayer)
add_subdirectory(kexiv2)
add_subdirectory(kpasswdstore)
######## kidletime ####################

View file

@ -0,0 +1,46 @@
if(OPENSSL_FOUND)
include_directories(${OPENSSL_INCLUDE_DIR})
add_definitions(-DHAVE_OPENSSL)
endif()
add_definitions(-DKDE_DEFAULT_DEBUG_AREA=51004)
set(kpasswdstore_LIB_SRCS
kpasswdstore.cpp
)
add_library(kpasswdstore ${LIBRARY_TYPE} ${kpasswdstore_LIB_SRCS})
target_link_libraries(kpasswdstore PUBLIC
${KDE4_KDECORE_LIBS}
${KDE4_KDEUI_LIBS}
)
if(OPENSSL_FOUND)
target_link_libraries(kpasswdstore PRIVATE ${OPENSSL_LIBRARIES})
endif()
set_target_properties(kpasswdstore PROPERTIES
VERSION ${GENERIC_LIB_VERSION}
SOVERSION ${GENERIC_LIB_SOVERSION}
)
generate_export_header(kpasswdstore)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/kpasswdstore_export.h
kpasswdstore.h
DESTINATION ${KDE4_INCLUDE_INSTALL_DIR}
COMPONENT Devel
)
install(
TARGETS kpasswdstore
EXPORT kdelibsLibraryTargets
${INSTALL_TARGETS_DEFAULT_ARGS}
)
if(ENABLE_TESTING)
add_subdirectory(tests)
endif()

View file

@ -0,0 +1,314 @@
/* This file is part of the KDE libraries
Copyright (C) 2022 Ivailo Monev <xakepa10@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2, as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "kpasswdstore.h"
#include "kstandarddirs.h"
#include "kconfiggroup.h"
#include "kpassworddialog.h"
#include "klocale.h"
#include "kdebug.h"
#include <QApplication>
#include <QElapsedTimer>
#include <QCryptographicHash>
#if defined(HAVE_OPENSSL)
# include <openssl/evp.h>
# include <openssl/err.h>
#endif
static const int kpasswdstore_buffsize = 1024;
static const int kpasswdstore_passtimeout = 30000;
// EVP_CIPHER_CTX_key_length() and EVP_CIPHER_CTX_iv_length() cannot be called
// prior to EVP_EncryptInit() and EVP_DecryptInit() so hardcoding these
static const int kpasswdstore_keylen = 32;
static const int kpasswdstore_ivlen = 16;
#if QT_VERSION >= 0x041200
static const QCryptographicHash::Algorithm kpasswdstore_algorithm = QCryptographicHash::KAT;
#else
static const QCryptographicHash::Algorithm kpasswdstore_algorithm = QCryptographicHash::Sha1;
#endif
class KPasswdStorePrivate
{
public:
KPasswdStorePrivate();
~KPasswdStorePrivate();
bool ensurePasswd();
QString decryptPasswd(const QString &passwd, bool *ok);
QString encryptPasswd(const QString &passwd, bool *ok);
bool cacheonly;
QString storeid;
QString passwdstore;
QMap<QByteArray, QString> cache;
private:
#if defined(HAVE_OPENSSL)
static QByteArray genBytes(const QByteArray &data, const int length);
QByteArray m_passwd;
QByteArray m_passwdiv;
QElapsedTimer m_passwdtimer;
#endif
};
KPasswdStorePrivate::KPasswdStorePrivate()
: cacheonly(false),
storeid(QApplication::applicationName()),
passwdstore(KStandardDirs::locateLocal("data", "kpasswdstore.ini"))
{
#if defined(HAVE_OPENSSL)
ERR_load_ERR_strings();
EVP_add_cipher(EVP_aes_256_cbc());
#endif
}
KPasswdStorePrivate::~KPasswdStorePrivate()
{
}
bool KPasswdStorePrivate::ensurePasswd()
{
#if defined(HAVE_OPENSSL)
if (!m_passwd.isEmpty() && m_passwdtimer.elapsed() >= kpasswdstore_passtimeout) {
m_passwd.clear();
}
m_passwdtimer.restart();
if (m_passwd.isEmpty()) {
KPasswordDialog kpasswddialog;
kpasswddialog.setPrompt(i18n("Enter a password for <b>%1</b> password storage", storeid));
if (!kpasswddialog.exec()) {
return false;
}
m_passwd = kpasswddialog.password().toUtf8();
if (m_passwd.isEmpty()) {
kWarning() << "Password is empty";
return false;
}
m_passwdiv = m_passwd.toHex();
}
return !m_passwd.isEmpty();
#else
// not used
return true;
#endif
}
QString KPasswdStorePrivate::decryptPasswd(const QString &passwd, bool *ok)
{
#if defined(HAVE_OPENSSL)
EVP_CIPHER_CTX *opensslctx = EVP_CIPHER_CTX_new();
if (Q_UNLIKELY(!opensslctx)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
return QString();
}
const QByteArray opensslpass = KPasswdStorePrivate::genBytes(m_passwd, kpasswdstore_keylen);
const QByteArray openssliv = KPasswdStorePrivate::genBytes(m_passwdiv, kpasswdstore_ivlen);
int opensslresult = EVP_DecryptInit(
opensslctx, EVP_aes_256_cbc(),
reinterpret_cast<const uchar*>(opensslpass.constData()),
reinterpret_cast<const uchar*>(openssliv.constData())
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
// qDebug() << Q_FUNC_INFO << EVP_CIPHER_CTX_key_length(opensslctx);
// qDebug() << Q_FUNC_INFO << EVP_CIPHER_CTX_iv_length(opensslctx);
Q_ASSERT(EVP_CIPHER_CTX_key_length(opensslctx) == kpasswdstore_keylen);
Q_ASSERT(EVP_CIPHER_CTX_iv_length(opensslctx) == kpasswdstore_ivlen);
const QByteArray passwdbytes = QByteArray::fromHex(passwd.toUtf8());
const int opensslbuffersize = (kpasswdstore_buffsize * EVP_CIPHER_CTX_block_size(opensslctx));
uchar opensslbuffer[opensslbuffersize];
::memset(opensslbuffer, 0, sizeof(opensslbuffer) * sizeof(uchar));
int opensslbufferpos = 0;
opensslresult = EVP_DecryptUpdate(
opensslctx,
opensslbuffer, &opensslbufferpos,
reinterpret_cast<const uchar*>(passwdbytes.constData()), passwdbytes.size()
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
opensslresult = EVP_DecryptFinal(
opensslctx,
opensslbuffer + opensslbufferpos, &opensslbufferpos
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
const QString result = QString::fromLatin1(reinterpret_cast<char*>(opensslbuffer), opensslbufferpos);
EVP_CIPHER_CTX_free(opensslctx);
*ok = !result.isEmpty();
return result;
#else
const QByteArray decrypted = QByteArray::fromBase64(passwd.toUtf8());
const QString result = QString::fromLatin1(decrypted.constData(), decrypted.size());
*ok = !result.isEmpty();
return result;
#endif
}
QString KPasswdStorePrivate::encryptPasswd(const QString &passwd, bool *ok)
{
#if defined(HAVE_OPENSSL)
EVP_CIPHER_CTX *opensslctx = EVP_CIPHER_CTX_new();
if (Q_UNLIKELY(!opensslctx)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
return QString();
}
const QByteArray opensslpass = KPasswdStorePrivate::genBytes(m_passwd, kpasswdstore_keylen);
const QByteArray openssliv = KPasswdStorePrivate::genBytes(m_passwdiv, kpasswdstore_ivlen);
int opensslresult = EVP_EncryptInit(
opensslctx, EVP_aes_256_cbc(),
reinterpret_cast<const uchar*>(opensslpass.constData()),
reinterpret_cast<const uchar*>(openssliv.constData())
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
Q_ASSERT(EVP_CIPHER_CTX_key_length(opensslctx) == kpasswdstore_keylen);
Q_ASSERT(EVP_CIPHER_CTX_iv_length(opensslctx) == kpasswdstore_ivlen);
const QByteArray passwdbytes = passwd.toUtf8();
const int opensslbuffersize = (kpasswdstore_buffsize * EVP_CIPHER_CTX_block_size(opensslctx));
uchar opensslbuffer[opensslbuffersize];
::memset(opensslbuffer, 0, sizeof(opensslbuffer) * sizeof(uchar));
int opensslbufferpos = 0;
opensslresult = EVP_EncryptUpdate(
opensslctx,
opensslbuffer, &opensslbufferpos,
reinterpret_cast<const uchar*>(passwdbytes.constData()), passwdbytes.size()
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
opensslresult = EVP_EncryptFinal(
opensslctx,
opensslbuffer + opensslbufferpos, &opensslbufferpos
);
if (Q_UNLIKELY(opensslresult != 1)) {
kWarning() << ERR_error_string(ERR_get_error(), NULL);
EVP_CIPHER_CTX_free(opensslctx);
return QString();
}
const QString result = QString::fromLatin1(QByteArray(reinterpret_cast<char*>(opensslbuffer), opensslbufferpos).toHex());
EVP_CIPHER_CTX_free(opensslctx);
*ok = !result.isEmpty();
return result;
#else
const QString result = passwd.toUtf8().toBase64();
*ok = !result.isEmpty();
return result;
#endif
}
#if defined(HAVE_OPENSSL)
QByteArray KPasswdStorePrivate::genBytes(const QByteArray &data, const int length)
{
const QByteArray result = QCryptographicHash::hash(data, kpasswdstore_algorithm).toHex();
Q_ASSERT(result.size() >= length);
return result.mid(length);
}
#endif
KPasswdStore::KPasswdStore(QObject *parent)
: QObject(parent),
d(new KPasswdStorePrivate())
{
}
KPasswdStore::~KPasswdStore()
{
delete d;
}
void KPasswdStore::setStoreID(const QString &id)
{
d->storeid = id;
}
void KPasswdStore::setCacheOnly(const bool cacheonly)
{
d->cacheonly = cacheonly;
d->cache.clear();
}
QString KPasswdStore::getPasswd(const QByteArray &key) const
{
if (!d->ensurePasswd()) {
return QString();
}
if (d->cacheonly) {
return d->cache.value(key, QString());
}
bool ok = false;
KConfig kconfig(d->passwdstore);
const QString passwd = kconfig.group(d->storeid).readEntry(key.constData(), QString());
return d->decryptPasswd(passwd, &ok);
}
bool KPasswdStore::storePasswd(const QByteArray &key, const QString &passwd)
{
if (!d->ensurePasswd()) {
return false;
}
if (d->cacheonly) {
d->cache.insert(key, passwd);
return true;
}
bool ok = false;
KConfig kconfig(d->passwdstore);
KConfigGroup kconfiggroup = kconfig.group(d->storeid);
kconfiggroup.writeEntry(key.constData(), d->encryptPasswd(passwd, &ok));
kconfiggroup.sync();
return ok;
}
QByteArray KPasswdStore::makeKey(const QString &string)
{
return QCryptographicHash::hash(string.toUtf8(), kpasswdstore_algorithm).toHex();
}

View file

@ -0,0 +1,86 @@
/* This file is part of the KDE libraries
Copyright (C) 2022 Ivailo Monev <xakepa10@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2, as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#ifndef KPASSWDSTORE_H
#define KPASSWDSTORE_H
#include "kpasswdstore_export.h"
#include <QObject>
class KPasswdStorePrivate;
/*!
Class to store and retrieve passwords.
The password used for encrypting and decrypting the store will be asked for
upon the first request and again after 30sec of inactivity.
@code
KPasswdStore kpasswdstore;
kpasswdstore.setStoreID("myid");
qDebug() << kpasswdstore.storePasswd("mykey", "mypass");
qDebug() << kpasswdstore.getPasswd("mykey");
@endcode
@since 4.21
@warning the API is subject to change
*/
class KPASSWDSTORE_EXPORT KPasswdStore : public QObject
{
public:
/*!
@brief Contructs object with @p parent
*/
KPasswdStore(QObject *parent = nullptr);
~KPasswdStore();
/*!
@brief Sets the store ID to @p id
@note The ID is @p QApplication::applicationName() by default
*/
void setStoreID(const QString &id);
/*!
@brief If @p cacheonly is @p true then no permanent storage is used and
passwords store is discarded when the object is destroyed. Whenever
called the cache is also cleared
@note Caching only is disabled by default
*/
void setCacheOnly(const bool cacheonly);
/*!
@brief Retrieves passwd for the give @p key from the password store
*/
QString getPasswd(const QByteArray &key) const;
/*!
@brief Stores @p passwd with the give @p key in the password store
*/
bool storePasswd(const QByteArray &key, const QString &passwd);
/*!
@brief Makes a unique key from @p string for use with @p getPasswd() and @p storePasswd()
*/
static QByteArray makeKey(const QString &string);
private:
Q_DISABLE_COPY(KPasswdStore);
KPasswdStorePrivate *d;
};
#endif // KPASSWDSTORE_H

View file

@ -0,0 +1,8 @@
include_directories(
${CMAKE_CURRENT_SOURCE_DIR}/..
)
kde4_add_manual_test(kpasswdstore-manual
kpasswdstore-manual.cpp
)
target_link_libraries(kpasswdstore-manual ${KDE4_KPASSWDSTORE_LIBS})

View file

@ -0,0 +1,46 @@
/* This file is part of the KDE libraries
Copyright (C) 2022 Ivailo Monev <xakepa10@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License version 2, as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include <QDebug>
#include <QApplication>
#include "kpasswdstore.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QString firstpass;
{
KPasswdStore kpasswdstore;
kpasswdstore.setStoreID("myid");
qDebug() << kpasswdstore.storePasswd("mykey", "dasasd");
firstpass = kpasswdstore.getPasswd("mykey");
qDebug() << firstpass;
}
{
QString secondpass;
KPasswdStore kpasswdstore;
kpasswdstore.setStoreID("myid");
secondpass = kpasswdstore.getPasswd("mykey");
qDebug() << firstpass << secondpass;
}
return app.exec();
}