feat: finished version 0.0.1

This commit is contained in:
2026-01-22 13:42:34 +03:00
parent 3241dd674d
commit eb43364f0f
17 changed files with 1511 additions and 37 deletions

View File

@@ -6,6 +6,8 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(OpenSSL REQUIRED)
find_package(Qt5 COMPONENTS
Core
Gui
@@ -20,6 +22,7 @@ file(GLOB
"src/MainWidget/*"
"src/LicenseModel/*"
"src/EditClientDialog/*"
"src/LicenseGenerator/*"
"deps/deps.qrc"
)
@@ -40,6 +43,8 @@ target_link_libraries(
Qt5::Gui
Qt5::Sql
Qt5::Widgets
OpenSSL::Crypto
OpenSSL::SSL
)
find_package(Git QUIET)

BIN
deps/Key vendored Normal file

Binary file not shown.

2
deps/deps.qrc vendored
View File

@@ -8,6 +8,8 @@
<file alias="delete.png">delete.png</file>
<file alias="info.png">info.png</file>
<file alias="dropFiles.png">dropFiles.png</file>
<file alias="getLicenseFile.png">getLicenseFile.png</file>
<file alias="tables.ddl">tables.ddl</file>
<file alias="Key">Key</file>
</qresource>
</RCC>

BIN
deps/getLicenseFile.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

1
deps/tables.ddl vendored
View File

@@ -8,5 +8,6 @@ CREATE TABLE IF NOT EXISTS clients (
patronymic TEXT NOT NULL,
phone TEXT NOT NULL,
yourCompany TEXT NOT NULL,
licenseTime TEXT,
comment TEXT
);

View File

@@ -1,6 +1,8 @@
#include "EditClientDialog.h"
// Qt
#include <QCalendarWidget>
#include <QCheckBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
@@ -97,7 +99,28 @@ EditClientDialog::EditClientDialog(LicenseModel* model, QWidget *parent)
m_commentTextEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
gridLayout->setRowStretch(11, 1);
m_tabWidget->addTab(m_manualWidget, tr("Manual"));
m_tabWidget->addTab(m_manualWidget, tr("Main info"));
}
{
auto layout = new QVBoxLayout();
auto licenseTimeWidget = new QWidget(this);
licenseTimeWidget->setLayout(layout);
m_useTemporaryLicenseCheckBox = new QCheckBox(tr("License is temporally"), licenseTimeWidget);
layout->addWidget(m_useTemporaryLicenseCheckBox);
m_calendarWidget = new QCalendarWidget(licenseTimeWidget);
layout->addWidget(m_calendarWidget);
m_calendarWidget->setEnabled(false);
m_calendarWidget->setGridVisible(true);
m_calendarWidget->showToday();
connect(m_useTemporaryLicenseCheckBox, &QCheckBox::stateChanged, [&](int state) {
m_calendarWidget->setEnabled(state == Qt::Checked);
});
m_tabWidget->addTab(licenseTimeWidget, tr("License time"));
}
auto hLayout = new QHBoxLayout;
@@ -160,8 +183,6 @@ void EditClientDialog::onFilesChanged(const QStringList &paths)
m_cityLineEdit->setText(configBody.value("city").toString());
if (configBody.contains("yourCompany"))
m_yourCompanyNameTextEdit->setText(configBody.value("yourCompany").toString());
if (configBody.contains("createdAtUtc"))
m_createdAtUtc = configBody.value("createdAtUtc").toString();
if (configBody.contains("hardwareHash"))
m_hardwareHashLineEdit->setText(configBody.value("hardwareHash").toString());
@@ -178,6 +199,19 @@ QLabel* EditClientDialog::makeLabel(const QString &text)
void EditClientDialog::setType(Type type)
{
m_type = type;
m_hardwareHashLineEdit->setEnabled(m_type == Type::Add);
switch (m_type)
{
case Type::Add:
m_tabWidget->setCurrentIndex(0);
break;
case Type::Edit:
m_tabWidget->setCurrentIndex(1);
break;
default:
break;
}
}
EditClientDialog::Type EditClientDialog::getType() const
@@ -196,7 +230,8 @@ LicenseModel::LicenseItem EditClientDialog::getLicenseItem() const
item.city = m_cityLineEdit->text().trimmed();
item.yourCompany = m_yourCompanyNameTextEdit->text().trimmed();
item.hardwareHash = m_hardwareHashLineEdit->text().trimmed();
item.createdAtUtc = m_createdAtUtc;
if (m_useTemporaryLicenseCheckBox->isChecked())
item.licenseTime = m_calendarWidget->selectedDate().toString();
item.comment = m_commentTextEdit->toPlainText().trimmed();
return item;
}
@@ -212,6 +247,8 @@ void EditClientDialog::clear()
m_yourCompanyNameTextEdit->clear();
m_hardwareHashLineEdit->clear();
m_commentTextEdit->clear();
m_useTemporaryLicenseCheckBox->setChecked(false);
m_calendarWidget->showToday();
m_configPathLabel->setText(tr("Drop file here"));
}
@@ -229,6 +266,16 @@ void EditClientDialog::setClientInfo(int index)
m_cityLineEdit->setText(item.city);
m_yourCompanyNameTextEdit->setText(item.yourCompany);
m_hardwareHashLineEdit->setText(item.hardwareHash);
if (item.licenseTime.isEmpty())
{
m_useTemporaryLicenseCheckBox->setChecked(false);
m_calendarWidget->showToday();
}
else
{
m_useTemporaryLicenseCheckBox->setChecked(true);
m_calendarWidget->setSelectedDate(QDate::fromString(item.licenseTime));
}
m_configPathLabel->setText(tr("Drop file here"));
}

View File

@@ -2,12 +2,15 @@
#define LICENSEMANAGER_EDITCLIENTDIALOG_H
// Qt
#include <QDate>
#include <QDebug>
#include <QDialog>
#include <QDragEnterEvent>
#include <QLabel>
#include "LicenseModel/LicenseModel.h"
class QCalendarWidget;
class QCheckBox;
class QLineEdit;
class QTabWidget;
class QTextEdit;
@@ -55,9 +58,10 @@ private:
QLineEdit* m_sellerNameTextEdit{nullptr}; //!< Поле ввода названия продавца (если используется).
QLineEdit* m_cityLineEdit{nullptr}; //!< Поле ввода города.
QLineEdit* m_hardwareHashLineEdit{nullptr}; //!< Поле для отображения хеша оборудования.
QCheckBox* m_useTemporaryLicenseCheckBox{nullptr};//!<
QCalendarWidget* m_calendarWidget{nullptr}; //!<
QTextEdit* m_commentTextEdit{nullptr}; //!<
QPushButton* m_saveButton{nullptr}; //!<
QString m_createdAtUtc;
QString m_filesPath;
Type m_type = Type::None;
};

View File

@@ -0,0 +1,53 @@
#include "LicenseGenerator.h"
#include "crypt.h"
#include "Util.h"
LicenseGenerator::Result LicenseGenerator::createEncryptedFile(const QString& outputFile,
const QString& sshTarget,
const QString& password,
const RunSshFn& runSsh)
{
Result result;
QByteArray text;
if (!runSsh("ssh",
QStringList() << sshTarget << "sed -rn 's/Serial\\t\\t: (.+)/\\1/p' /proc/cpuinfo",
password,
"get cpu id",
&text))
{
result.error = Error::SshFailed;
return result;
}
result.cpuId = text;
if (text.isEmpty())
{
result.error = Error::CpuIdEmpty;
return result;
}
if ((text.length() != 16 && text.length() != 20) || findFirstNotOf(text, "0123456789abcdef") >= 0)
{
result.error = Error::CpuIdInvalid;
return result;
}
QByteArray encrypted = Crypt::rsaEncrypt(text, fileReadAll(":/deps/Key"));
if (encrypted.isEmpty())
{
result.error = Error::EncryptFailed;
return result;
}
if (!fileWriteAll(outputFile, encrypted))
{
result.error = Error::WriteFailed;
return result;
}
result.error = Error::Ok;
return result;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <functional>
#include <QByteArray>
#include <QString>
#include <QStringList>
class LicenseGenerator
{
public:
enum class Error {
Ok,
SshFailed,
CpuIdEmpty,
CpuIdInvalid,
EncryptFailed,
WriteFailed
};
struct Result {
Error error = Error::Ok;
QByteArray cpuId;
};
using RunSshFn = std::function<bool(const QByteArray&, const QStringList&, const QString&, const QString&, QByteArray*)>;
static Result createEncryptedFile(const QString& outputFile,
const QString& sshTarget,
const QString& password,
const RunSshFn& runSsh);
};

View File

@@ -0,0 +1,410 @@
#include "Util.h"
#include <random>
#include <optional>
#include <QRegularExpression>
#include <QElapsedTimer>
#include <QCryptographicHash>
#include <QCoreApplication>
#include <QCborMap>
#include <QCborArray>
QByteArray fileReadAll(const QString& path)
{
QFile f(path);
QByteArray buf;
if (f.open(QIODevice::ReadOnly))
buf = f.readAll();
return buf;
}
bool fileWriteAll(const QString& path, const QByteArray& data, int bufOffset)
{
QFile f(path);
if (!f.exists()) {
QFileInfo fi(f);
QDir d;
d.mkpath(fi.dir().path());
}
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate))
return false;
int size = data.size() - bufOffset;
int bytesWritten = f.write(data.constData() + bufOffset, size);
f.close();
return bytesWritten == size;
}
bool splitAddr(const QString& addrOrNetwork, bool isAddress, uint& addr, uint* mask)
{
QString s = "^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})";
if (mask)
s += "/(\\d\\d?)";
s += '$';
QRegularExpression re(s, QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(addrOrNetwork);
if (!m.hasMatch()) return false;
uint a = m.captured(1).toUInt();
if (a == 0 || a > 255) return false;
uint b = m.captured(2).toUInt();
if (b > 255) return false;
uint c = m.captured(3).toUInt();
if (c > 255) return false;
uint d = m.captured(4).toUInt();
if ((isAddress && d == 0) || d >= 255) return false;
if (mask)
{
*mask = m.captured(5).toInt();
if (*mask == 0 || *mask > 30) return false;
}
addr = (a << 24) | (b << 16) | (c << 8) | d;
return true;
}
bool ipOk(const QString& addr, bool netmask)
{
uint a;
if (netmask)
{
uint m;
return splitAddr(addr, true, a, &m);
}
return splitAddr(addr, true, a);
}
QString makeHash(const QString& password)
{
std::random_device rd;
QString result;
while(result.length() < 16) // salt
{
uint a = rd() % 52;
result += QChar(a < 26 ? ('a' + a) : ('A' + a - 26));
}
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(result.toLatin1());
hash.addData(password.toUtf8());
(result += '$') += hash.result().toHex();
return result;
}
bool testPassword(const QByteArray& password, const QByteArray& passwordHash)
{
int j = passwordHash.indexOf('$');
if (j < 0) return false;
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(passwordHash.mid(0, j));
hash.addData(password);
const QByteArray h = hash.result();
return std::equal(h.constData(), h.constData() + h.size(), passwordHash.constData() + j + 1);
}
int findFirstNotOf(const QString& s, const char* chars, int pos, int n)
{
if (n >= 0) n += pos;
else n = s.length();
for(; pos < n; ++pos)
for(const char* p = chars;; p++)
{
if (!*p) return pos;
if (s[pos] == char16_t(*p)) break;
}
return -1;
}
int findFirstNotOf(const QString& s, QChar c, int pos, int n)
{
if (n >= 0) n += pos;
else n = s.length();
for(; pos < n; ++pos)
{
if (s[pos] != c)
return pos;
}
return -1;
}
QByteArray& trimLeft(QByteArray& s, char c)
{
int i = 0, length = s.length();
for(; i < length; ++i)
if (s.at(i) != c)
break;
s.remove(0, i);
return s;
}
QByteArray& trimRight(QByteArray& s, char c)
{
if (!s.isEmpty())
{
int i = s.length() - 1;
for(; i >= 0; --i)
if (s.at(i) != c)
break;
i++;
s.resize(i);
}
return s;
}
QByteArray& trimLeft(QByteArray& s, const char* chars) // ascii only
{
int i = 0, length = s.length();
for(; i < length; ++i)
{
QChar c = s.at(i);
for(const char* p = chars;; p++)
{
if (!*p) goto breakfor;
if (char16_t(*p) == c) break;
}
}
breakfor:
s.remove(0, i);
return s;
}
QByteArray& trimRight(QByteArray& s, const char* chars) // ascii only
{
if (!s.isEmpty())
{
int i = s.length() - 1;
for(; i >= 0; --i)
{
QChar c = s.at(i);
for(const char* p = chars;; p++)
{
if (!*p) goto breakfor;
if (char16_t(*p) == c) break;
}
}
breakfor:
i++;
s.resize(i);
}
return s;
}
QString& trimLeft(QString& s, QChar c)
{
int i = 0, length = s.length();
for(; i < length; ++i)
if (s.at(i) != c)
break;
s.remove(0, i);
return s;
}
QString& trimRight(QString& s, QChar c)
{
if (!s.isEmpty())
{
int i = s.length() - 1;
for(; i >= 0; --i)
if (s.at(i) != c)
break;
i++;
s.resize(i);
}
return s;
}
QString& append(QString& s, const QVector<int>& v, QChar sep)
{
if (!v.isEmpty())
{
for(int i = 0; i < v.size(); ++i)
(s += QString::number(v[i])) += sep;
s.resize(s.size() - 1);
}
return s;
}
QVector<int> splitToInts(const QString& s, QChar sep)
{
QVector<int> r;
if (!s.isEmpty())
for(const QString& ss : s.split(sep))
r << ss.toInt();
return r;
}
QString getPath(const QString& path)
{
int i = path.length();
if (i == 0 || i == 1) return {};
--i;
if (path[i] == '/') --i;
i = path.lastIndexOf('/', i);
if (i == 0) return QString(1, '/');
if (i < 0) return QString();
return path.mid(0, i);
}
QString getFileName(const QString& path)
{
if (path.isEmpty()) return {};
int i = path.lastIndexOf('/');
return i >= 0 ? path.mid(i + 1) : path;
}
QJsonArray convertVectorToJsonArray(const QVector<bool> &vector)
{
QJsonArray resultArray;
for (const bool & value: vector){
resultArray.append(value);
}
return resultArray;
}
QVector<bool> convertJsonArrayToVector(const QJsonArray &array)
{
QVector<bool> vector;
for (int i = 0; i < array.count(); i++){
vector.append(array[i].toBool());
}
return vector;
}
QByteArray addSizeToByteArray(const QByteArray &arr)
{
QByteArray byteArray;
QDataStream stream(&byteArray, QIODevice::WriteOnly);
stream << arr.size();
return byteArray + arr;
}
int getSizeJsonInByteArr(const QByteArray &arr)
{
int size = -1;
if (arr.size() > 4) {
QDataStream ds(arr.mid(0, sizeof(int)));
ds >> size;
}
return size;
}
QString &append(QString &s, const QVector<QByteArray> &v, QChar sep)
{
if (!v.isEmpty())
{
for(int i = 0; i < v.size(); ++i)
(s += QString(v[i])) += sep;
s.resize(s.size() - 1);
}
return s;
}
QByteArray toCbor(const QJsonObject &jsonObject)
{
QCborValue cborValue = QCborValue::fromJsonValue(QJsonValue(jsonObject));
return cborValue.toCbor();
}
QByteArray toCbor(const QJsonArray &jsonArray)
{
QCborValue cborValue = QCborValue::fromJsonValue(QJsonValue(jsonArray));
return cborValue.toCbor();
}
QByteArray toCbor(const QJsonDocument &doc)
{
if (doc.isObject()) {
return toCbor(doc.object());
} else if (doc.isArray()) {
return toCbor(doc.array());
}
return {}; // Return an empty QByteArray if the document is invalid
}
QJsonDocument fromCbor(const QByteArray &cborData)
{
QCborValue cborValue = QCborValue::fromCbor(cborData);
if (cborValue.isMap()) {
return QJsonDocument(cborValue.toMap().toJsonObject());
} else if (cborValue.isArray()) {
return QJsonDocument(cborValue.toArray().toJsonArray());
}
// Return an empty QJsonDocument if the type is unsupported
return {};
}
QVector<int> arrayFromMask(uint8_t mask)
{
int idx=0;
QVector<int> ret;
while (mask) {
if(mask & 1) ret.append(idx);
mask>>=1;
idx++;
}
return ret;
}
QVector<int> intersectUnique(const QVector<int> &a, const QVector<int> &b) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QSet<int> sa(a.cbegin(), a.cend());
QSet<int> sb(b.cbegin(), b.cend());
#else
QSet<int> sa = QSet<int>::fromList(a.toList());
QSet<int> sb = QSet<int>::fromList(b.toList());
#endif
// In-place intersection; 'sa' becomes the result
sa.intersect(sb);
QVector<int> out;
out.reserve(sa.size());
for (int v : qAsConst(sa)) out.append(v);
// Optional: deterministic order
std::sort(out.begin(), out.end());
return out;
}
int popCount(const QVector<bool> &vec, bool value)
{
int count = 0;
for (bool v : vec) {
if (v == value)
++count;
}
return count;
}
QVector<bool> vectorOr(const QVector<bool> &v1, const QVector<bool> &v2)
{
Q_ASSERT(v1.size() == v2.size()); // sanity check
QVector<bool> v3(v1.size());
std::transform(v1.cbegin(), v1.cend(), v2.cbegin(), v3.begin(),
[](bool x, bool y){ return x || y; });
return v3;
}
QVector<bool> invert(const QVector<bool> &v)
{
QVector<bool> result(v.size());
std::transform(v.cbegin(), v.cend(), result.begin(),
[](bool b){ return !b; });
return result;
}
QString toString(const QVector<bool> &vec, bool value)
{
QStringList out;
for (int i = 0; i < vec.size(); ++i) {
if (vec[i] == value) {
out << QString::number(i + 1); // 1-based index
}
}
return out.join(", ");
}

304
src/LicenseGenerator/Util.h Normal file
View File

@@ -0,0 +1,304 @@
#pragma once
#include <QFile>
#include <QDateTime>
#include <QtNetwork/QTcpSocket>
#include <vector>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QDataStream>
#include <deque>
#include <optional>
template <typename T>
class FifoContainer {
public:
explicit FifoContainer(std::size_t maxSize) : m_maxSize(maxSize) {}
void add(T value)
{
if (m_container.size() >= m_maxSize)
m_container.pop_front(); // Remove the oldest element (FIFO)
m_container.push_back(value); // Add the new element
}
std::optional<double> getAverage() const
{
if (m_container.size() < m_maxSize) {
return std::nullopt; // Indicate that the container is not yet full
}
// Use std::accumulate to calculate the sum of the elements
T sum = std::accumulate(m_container.begin(), m_container.end(), T(0));
return static_cast<double>(sum) / m_container.size();
}
void clear() { m_container.clear(); }
std::deque<T> &container() { return m_container; }
operator QString() const {
QStringList l;
for (int i = 0; i < m_container.size(); ++i) {
l << QString::number(m_container[i]);
}
return l.join(", ");
}
private:
std::size_t m_maxSize; // Maximum size of the m_container
std::deque<T> m_container; // FIFO container
};
QByteArray fileReadAll(const QString& path);
inline QStringList fileReadAllLines(const QString& path)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
return QString(fileReadAll(path)).split('\n', Qt::SkipEmptyParts);
#else
return QString(fileReadAll(path)).split('\n', QString::SkipEmptyParts);
#endif
}
bool fileWriteAll(const QString& path, const QByteArray& data, int bufOffset = 0);
inline bool fileWriteAllLines(const QString& path, const QStringList& list) { return fileWriteAll(path, list.join('\n').toUtf8()); }
template <typename T, typename Cleanup = QScopedPointerDeleter<T>>
class Ptr : public QScopedPointer<T, Cleanup>
{
typedef QScopedPointer<T, Cleanup> base;
public:
using base::base;
operator T*() const { return base::data(); }
operator bool() const { return base::data() != 0; }
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
T* get() const { return base::data(); }
#endif
};
//inline bool isConnectingOrConnected(const QTcpSocket* s)
//{ return s->state() == QAbstractSocket::ConnectingState || s->state() == QAbstractSocket::ConnectedState; }
inline bool isConnected(const QTcpSocket* s) { return s->state() == QAbstractSocket::ConnectedState; }
QString getPath(const QString& path);
QString getFileName(const QString& path);
QString toStr(const QHostAddress& a);
bool splitAddr(const QString& addrOrNetwork, bool isAddress, uint& addr, uint* mask = 0);
bool ipOk(const QString& addr, bool netmask);
template<class T>
class Vector : public std::vector<T>
{
typedef std::vector<T> base;
public:
using base::base;
int size() const { return base::size(); }
void clear() { base::clear(); base::shrink_to_fit(); }
void remove(int pos) { /*checkIndex(pos);*/ base::erase(base::begin() + pos); }
};
template<class T>
class List : public std::list<T>
{
typedef std::list<T> base;
public:
using base::base;
int size() const { return base::size(); }
List& operator+=(const T& v) { base::push_back(v); return *this; }
List& operator+=(const List& v) { base::insert(base::end(), v.begin(), v.end()); return *this; }
List& operator<<(const T& v) { return *this += v; }
List& operator<<(const List& v) { return *this += v; }
};
namespace Alg
{
template<class C> inline void remove(C& c, const typename C::value_type& value)
{
typename C::iterator i = std::remove(c.begin(), c.end(), value);
c.erase(i, c.end());
}
template<class C, class P> inline void removeIf(C& c, P pred)
{
typename C::iterator i = std::remove_if(c.begin(), c.end(), pred);
c.erase(i, c.end());
}
template<class I, class T> inline bool contains(I first, I last, const T& value)
{
I i = std::find(first, last, value);
return i != last;
}
template<class I, class P> inline bool containsIf(I first, I last, P pred)
{
I i = std::find_if(first, last, pred);
return i != last;
}
template<class C> inline C& sort(C& c) { std::sort(c.begin(), c.end()); return c; }
template<class C, class P> inline C& sort(C& c, const P& pred) { std::sort(c.begin(), c.end(), pred); return c; }
template<class C> inline bool contains(const C& c, const typename C::value_type& value) { return contains(c.begin(), c.end(), value); }
template<class C, class P> inline bool containsIf(const C& c, P pred) { return containsIf(c.begin(), c.end(), pred); }
template<class C, class T> inline typename C::iterator find(C& c, const T& v) { return std::find(c.begin(), c.end(), v); }
template<class C, class T> inline typename C::const_iterator find(const C& c, const T& v) { return std::find(c.begin(), c.end(), v); }
template<class C, class P> inline typename C::iterator findIf(C& c, const P& pred) { return std::find_if(c.begin(), c.end(), pred); }
template<class C, class P> inline typename C::const_iterator findIf(const C& c, const P& pred) { return std::find_if(c.begin(), c.end(), pred); }
template<class C> inline int findIndex(const C& c, const typename C::value_type& v)
{
for(int i = 0; i < int(c.size()); ++i)
if (v == c[i])
return i;
return -1;
}
template<class C, class P> inline int findIndexIf(const C& c, const P& pred)
{
for(int i = 0; i < int(c.size()); ++i)
if (pred(c[i]))
return i;
return -1;
}
template<class C, class P> inline int rfindIndexIf(const C& c, const P& pred)
{
for(int i = int(c.size()) - 1; i >= 0; --i)
if (pred(c[i]))
return i;
return -1;
}
template<class C, class F> C& forEach(C& c, F f) { std::for_each(c.begin(), c.end(), f); return c; }
template<class C, class F> const C& forEach(const C& c, F f) { std::for_each(c.begin(), c.end(), f); return c; }
template<class C, typename T> inline void fill(C& c, const T& value) { std::fill(c.begin(), c.end(), value); }
template<class C, class It> It copy(C& c, It to) { return std::copy(c.begin(), c.end(), to); }
//template<class C, class P> C select(const C& v, P pred)
template<class T, class P> QVector<int> selectIndexes(const QVector<T>& v, P pred)
{
QVector<int> r;
for(int i = 0; i < int(v.size()); ++i)
if (pred(v[i]))
r << i;
return r;
}
template<class C, class P> inline int countIf(const C &c, const P &pred) { return std::count_if(c.begin(), c.end(), pred); }
}
// Returns pointer to value if the variant holds T, otherwise nullptr
template<typename T, typename Variant>
inline decltype(auto) get_if_type(Variant& v) {
return std::holds_alternative<T>(v) ? std::addressof(std::get<T>(v)) : nullptr;
}
template<typename T, typename Variant>
inline const T* get_if_polymorphic(const Variant& v) {
if (const auto* p = dynamic_cast<const T*>(std::addressof(v))) {
return p;
}
return nullptr;
}
const QString DateFormat = "dd.MM.yyyy", DtFormat = "dd.MM.yyyy HH:mm", DtFormatSec = "dd.MM.yyyy HH:mm:ss", DtFormatSecMsec = "ss:zzz";
bool isRu();
//inline QDate nowd() { return QDate::currentDate(); }
inline QTime nowt() { return QTime::currentTime(); }
inline QDateTime now() { return QDateTime::currentDateTime(); }
//inline QDateTime nowutc() { return QDateTime::currentDateTimeUtc(); }
//inline QDateTime today() { return QDateTime(nowd()); }
inline QString nowstr() { return now().toString(DtFormatSec); }
//inline QString nowdstr() { return nowd().toString(DateFormat); }
inline QString nowtstr() { return nowt().toString("hh:mm:ss"); }
QString makeHash(const QString& password);
bool testPassword(const QByteArray& password, const QByteArray& passwordHash);
//inline QDebug qs() { return qDebug().nospace(); }
//inline QDebug qq() { return qDebug().noquote(); }
//QByteArray calcHash(const QStringList& list);
inline int compareNoCase(const QString& a, const QString& b) { return a.compare(b, Qt::CaseInsensitive); }
int findFirstNotOf(const QString& s, const char* chars, int pos = 0, int n = -1);
int findFirstNotOf(const QString& s, QChar c, int pos = 0, int n = -1);
inline void noCaseSort(QStringList& list)
{ std::sort(list.begin(), list.end(), [](const QString& a, const QString& b) { int r = compareNoCase(a, b); return r == 0 ? (a < b) : r < 0; }); }
QByteArray& trimLeft(QByteArray& s, char c);
QByteArray& trimRight(QByteArray& s, char c);
inline QByteArray& trim(QByteArray& s, char c) { return trimLeft(trimRight(s, c), c); }
QByteArray& trimLeft(QByteArray& s, const char* chars); // ascii only
QByteArray& trimRight(QByteArray& s, const char* chars); // ascii only
inline QByteArray& trim(QByteArray& s, const char* chars) { return trimLeft(trimRight(s, chars), chars); }
QString& trimLeft(QString& s, QChar c);
QString& trimRight(QString& s, QChar c);
inline QString& trim(QString& s, QChar c) { return trimLeft(trimRight(s, c), c); }
//QString& trimLeft(QString& s, const char* chars); // ascii only
//QString& trimRight(QString& s, const char* chars); // ascii only
//inline QString& trim(QString& s, const char* chars) { return trimLeft(trimRight(s, chars), chars); }
//QString& trimLeft(QString& s, const char16_t* chars);
//QString& trimRight(QString& s, const char16_t* chars);
//inline QString& trim(QString& s, const char* chars) { return trimLeft(trimRight(s, chars), chars); }
QString& append(QString& s, const QVector<int>& v, QChar sep);
inline QString join(const QVector<int>& v, QChar sep)
{
QString s;
append(s, v, sep);
return s;
}
QString &append(QString &s, const QVector<QByteArray> &v, QChar sep);
QVector<int> splitToInts(const QString& s, QChar sep);
QVector<QHostAddress> getIpAddr();
QVector<bool> convertStringToVectorBool(const QString &string);
QByteArray addSizeToByteArray(const QByteArray &arr);
int getSizeJsonInByteArr(const QByteArray &arr);
QString toString(const QVector<bool> &vec, bool value);
//Counts values in bool vectors
int popCount(const QVector<bool> &vec, bool value);
QVector<bool> vectorOr(const QVector<bool> &a, const QVector<bool> &b);
QVector<bool> invert(const QVector<bool> &v);
QString execAndReadAll(const QString& prog, const QStringList& args, const QString& workDir, int waitTimeout = 0, bool ignoreErrorOut = false);
inline QString execAndReadAll1(const QString& prog, const QString& arg, int waitTimeout = 0, bool ignoreErrorOut = false) { return execAndReadAll(prog, QStringList(arg), {}, waitTimeout, ignoreErrorOut); }
QRegularExpressionMatch paCardMatchSinks();
QRegularExpressionMatch paPciMatchSinks();
QByteArray getPaUsbOutput();
QByteArray getPaUsbInput();
bool isUsbAudioCardPresent();
QByteArray getDefaultAudioInput();
QByteArray getDefaultAudioOutput();
QByteArray getDefaultAudioInputStr();
QByteArray getDefaultAudioOutputStr();
QByteArray getMachineId();
inline QString qStrFromBA(const QByteArray& chaddr) { return QString::fromUtf8(chaddr.left(6).toHex(':')); }
#ifndef Q_OS_WINDOWS
bool getVersion();
#endif
QJsonArray convertVectorToJsonArray(const QVector<bool> &vector);
QVector<bool> convertJsonArrayToVector(const QJsonArray &array);
QByteArray toCbor(const QJsonObject &jsonObject);
QByteArray toCbor(const QJsonArray &jsonArray);
QByteArray toCbor(const QJsonDocument &doc);
QJsonDocument fromCbor(const QByteArray &cborData);
QVector<int> arrayFromMask(uint8_t mask);
QVector<int> intersectUnique(const QVector<int>& a, const QVector<int>& b);

View File

@@ -0,0 +1,478 @@
#include "crypt.h"
#include <cmath>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <QDebug>
#include <QDataStream>
QPair<QByteArray, QByteArray> Crypt::generateRSAKeyPair() {
QPair<QByteArray, QByteArray> keyPair;
EVP_PKEY *keypair = nullptr;
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0 || EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0) {
qDebug() << "Failed to initialize RSA key generation context";
EVP_PKEY_CTX_free(ctx);
return keyPair;
}
if (EVP_PKEY_keygen(ctx, &keypair) <= 0) {
qDebug() << "Failed to generate RSA key pair";
EVP_PKEY_CTX_free(ctx);
return keyPair;
}
// Convert private key to DER format
BIO *privateBio = BIO_new(BIO_s_mem());
if (i2d_PrivateKey_bio(privateBio, keypair) <= 0) {
qDebug() << "Failed to write private key in DER format";
EVP_PKEY_free(keypair);
BIO_free(privateBio);
EVP_PKEY_CTX_free(ctx);
return keyPair;
}
char *privateData = nullptr;
long privateLen = BIO_get_mem_data(privateBio, &privateData);
keyPair.first = QByteArray(privateData, privateLen);
BIO_free(privateBio);
// Convert public key to DER format
BIO *publicBio = BIO_new(BIO_s_mem());
if (i2d_PUBKEY_bio(publicBio, keypair) <= 0) {
qDebug() << "Failed to write public key in DER format";
EVP_PKEY_free(keypair);
BIO_free(publicBio);
EVP_PKEY_CTX_free(ctx);
return keyPair;
}
char *publicData = nullptr;
long publicLen = BIO_get_mem_data(publicBio, &publicData);
keyPair.second = QByteArray(publicData, publicLen);
BIO_free(publicBio);
EVP_PKEY_free(keypair);
EVP_PKEY_CTX_free(ctx);
return keyPair;
}
QByteArray Crypt::generateAESKey() {
QByteArray aesKey(32, 0); // 32 bytes for AES-256
if (!RAND_bytes(reinterpret_cast<unsigned char*>(aesKey.data()), aesKey.size())) {
qDebug() << "Failed to generate AES key";
}
return aesKey;
}
bool Crypt::encryptAESKeyWithRSA(EVP_PKEY *publicKey, const unsigned char *aesKey, size_t aesKeyLength, QByteArray &encryptedKey) {
encryptedKey.resize(256); // For RSA 2048-bit
size_t encryptedKeyLength = encryptedKey.size();
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(publicKey, nullptr);
if (!ctx || EVP_PKEY_encrypt_init(ctx) <= 0) {
qDebug() << "Failed to initialize RSA encryption";
EVP_PKEY_CTX_free(ctx);
return false;
}
EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
if (EVP_PKEY_encrypt(ctx, reinterpret_cast<unsigned char*>(encryptedKey.data()), &encryptedKeyLength, aesKey, aesKeyLength) <= 0) {
qDebug() << "RSA encryption failed";
EVP_PKEY_CTX_free(ctx);
ERR_print_errors_fp(stderr);
return false;
}
encryptedKey.resize(encryptedKeyLength);
EVP_PKEY_CTX_free(ctx);
return true;
}
Crypt::EncryptedData Crypt::encrypt(const QByteArray &plaintext, const QByteArray &publicKey, const QByteArray &aesKey) {
EncryptedData result;
EVP_PKEY *evpPublicKey = loadKeyFromDER(publicKey, true);
if (!evpPublicKey) {
return result;
}
// Generate random IV
result.iv.resize(12); // GCM standard recommends a 12-byte IV
if (!RAND_bytes(reinterpret_cast<unsigned char*>(result.iv.data()), result.iv.size())) {
qDebug() << "Failed to generate random IV";
EVP_PKEY_free(evpPublicKey);
return result;
}
// Encrypt the AES key with RSA public key
if (!encryptAESKeyWithRSA(evpPublicKey, reinterpret_cast<const unsigned char*>(aesKey.data()), aesKey.size(), result.encryptedKey)) {
qDebug() << "Failed to encrypt AES key with RSA";
EVP_PKEY_free(evpPublicKey);
return result;
}
EVP_PKEY_free(evpPublicKey); // Free the EVP_PKEY structure for the public key
// AES-GCM encryption of the plaintext
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
qDebug() << "Failed to create encryption context";
return result;
}
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) <= 0 ||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, result.iv.size(), nullptr) <= 0 ||
EVP_EncryptInit_ex(ctx, nullptr, nullptr, reinterpret_cast<const unsigned char*>(aesKey.data()), reinterpret_cast<const unsigned char*>(result.iv.data())) <= 0) {
qDebug() << "Failed to initialize AES-GCM encryption";
EVP_CIPHER_CTX_free(ctx);
return result;
}
// Encrypt the plaintext
QByteArray ciphertext(plaintext.size() + EVP_CIPHER_block_size(EVP_aes_256_gcm()), 0);
int len = 0, ciphertext_len = 0;
if (EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()), &len, reinterpret_cast<const unsigned char*>(plaintext.data()), plaintext.size()) <= 0) {
qDebug() << "AES-GCM encryption failed at Update step";
EVP_CIPHER_CTX_free(ctx);
return result;
}
ciphertext_len += len;
// Finalize encryption
if (EVP_EncryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(ciphertext.data()) + len, &len) <= 0) {
qDebug() << "AES-GCM encryption failed at Final step";
EVP_CIPHER_CTX_free(ctx);
return result;
}
ciphertext_len += len;
ciphertext.resize(ciphertext_len);
result.encryptedData = ciphertext;
// Get the GCM authentication tag
result.tag.resize(16); // Standard GCM tag length is 16 bytes
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, result.tag.size(), result.tag.data()) <= 0) {
qDebug() << "Failed to retrieve GCM tag";
EVP_CIPHER_CTX_free(ctx);
return result;
}
EVP_CIPHER_CTX_free(ctx);
return result;
}
bool Crypt::decryptAESKeyWithRSA(EVP_PKEY *privateKey, const QByteArray &encryptedKey, unsigned char *aesKey, size_t aesKeyLength) {
// Ensure the buffer size is set to the RSA key size (e.g., 256 bytes for a 2048-bit RSA key)
unsigned char decryptedBuffer[256]; // Adjust size according to RSA key length, e.g., 256 bytes for RSA 2048-bit
size_t decryptedBufferSize = sizeof(decryptedBuffer);
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(privateKey, nullptr);
if (!ctx || EVP_PKEY_decrypt_init(ctx) <= 0) {
qDebug() << "Failed to initialize RSA decryption";
EVP_PKEY_CTX_free(ctx);
return false;
}
// Set RSA padding to OAEP, which is recommended for encrypting small data such as AES keys
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
qDebug() << "Failed to set RSA padding";
EVP_PKEY_CTX_free(ctx);
return false;
}
// Perform RSA decryption into the `decryptedBuffer`
if (EVP_PKEY_decrypt(ctx, decryptedBuffer, &decryptedBufferSize, reinterpret_cast<const unsigned char*>(encryptedKey.data()), encryptedKey.size()) <= 0) {
qDebug() << "RSA decryption failed";
EVP_PKEY_CTX_free(ctx);
ERR_print_errors_fp(stderr); // Print detailed OpenSSL error messages
return false;
}
// Check if the decrypted data fits the expected AES key size
if (decryptedBufferSize < aesKeyLength) {
qDebug() << "Decrypted data is too small for AES key";
EVP_PKEY_CTX_free(ctx);
return false;
}
// Copy only the first `aesKeyLength` bytes to `aesKey`
memcpy(aesKey, decryptedBuffer, aesKeyLength);
EVP_PKEY_CTX_free(ctx);
return true;
}
QByteArray Crypt::decrypt(const EncryptedData &encryptedData, const QByteArray &privateKey) {
QByteArray decryptedText;
EVP_PKEY *evpPrivateKey = loadKeyFromDER(privateKey, false);
if (!evpPrivateKey) {
return QByteArray();
}
// Decrypt the AES key using RSA private key
unsigned char aesKey[32]; // Buffer to hold the 32-byte AES key
if (!decryptAESKeyWithRSA(evpPrivateKey, encryptedData.encryptedKey, aesKey, sizeof(aesKey))) {
qDebug() << "Failed to decrypt AES key with RSA";
EVP_PKEY_free(evpPrivateKey);
return QByteArray();
}
EVP_PKEY_free(evpPrivateKey); // Free the EVP_PKEY structure for the private key
// AES-GCM decryption
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
qDebug() << "Failed to create decryption context";
return QByteArray();
}
// Initialize decryption with AES-256-GCM
if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) <= 0 ||
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, encryptedData.iv.size(), nullptr) <= 0 ||
EVP_DecryptInit_ex(ctx, nullptr, nullptr, aesKey, reinterpret_cast<const unsigned char*>(encryptedData.iv.data())) <= 0) {
qDebug() << "Failed to initialize AES-GCM decryption";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
// Decrypt the encrypted data
decryptedText.resize(encryptedData.encryptedData.size());
int len = 0, plaintext_len = 0;
if (EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(decryptedText.data()), &len,
reinterpret_cast<const unsigned char*>(encryptedData.encryptedData.data()), encryptedData.encryptedData.size()) <= 0) {
qDebug() << "AES-GCM decryption failed at Update step";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
plaintext_len += len;
// Set the expected tag for verification
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, encryptedData.tag.size(),
const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(encryptedData.tag.data()))) <= 0) {
qDebug() << "Failed to set GCM tag for verification";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
// Finalize decryption and verify tag
if (EVP_DecryptFinal_ex(ctx, reinterpret_cast<unsigned char*>(decryptedText.data()) + plaintext_len, &len) <= 0) {
qDebug() << "Tag verification failed or decryption unsuccessful";
EVP_CIPHER_CTX_free(ctx);
return QByteArray();
}
plaintext_len += len;
EVP_CIPHER_CTX_free(ctx);
decryptedText.resize(plaintext_len); // Adjust the QByteArray to the actual plaintext length
return decryptedText;
}
QByteArray Crypt::decrypt(const QByteArray &encryptedData, const QByteArray &privateKey)
{
EncryptedData ed;
QDataStream ds(encryptedData);
ds >> ed;
return decrypt(ed, privateKey);
}
bool Crypt::isValidPublicKeyDER(const QByteArray &derKeyData)
{
// Create a memory BIO from the QByteArray
BIO *bio = BIO_new_mem_buf(derKeyData.constData(), derKeyData.size());
if (!bio) {
qWarning("Failed to create BIO");
return false;
}
// Attempt to decode the DER-encoded data as a public key
EVP_PKEY *pkey = d2i_PUBKEY_bio(bio, nullptr);
BIO_free(bio); // Free the BIO after use
bool isValid = (pkey != nullptr); // If pkey is non-null, its a valid key
if (pkey) {
EVP_PKEY_free(pkey); // Free the EVP_PKEY structure if it was valid
}
return isValid;
}
QByteArray Crypt::rsaDecrypt(const QByteArray &encryptedData, const QByteArray &publicKey)
{
EVP_PKEY *pkey = loadKeyFromDER(publicKey, true);
if (!pkey) {
return QByteArray();
}
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
if (!ctx) {
EVP_PKEY_free(pkey);
qDebug() << "Failed to create EVP_PKEY_CTX for decryption.";
ERR_print_errors_fp(stderr);
return QByteArray();
}
const int key_bits =
#if OPENSSL_VERSION_NUMBER < 0x30000000L
EVP_PKEY_bits(pkey);
#else
EVP_PKEY_get_bits(pkey);
#endif
qDebug() << "Key bits" << key_bits;
EVP_PKEY_free(pkey);
int chunkSize = key_bits / 8;
if (EVP_PKEY_verify_recover_init(ctx) <= 0) {
qDebug() << "Failed to initialize verification context.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) {
qDebug() << "Failed to set RSA padding.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
QByteArray ret;
for (int i = 0; i < encryptedData.size() / chunkSize; ++i) {
int offset = i * chunkSize;
QByteArray chunk = encryptedData.mid(offset, chunkSize);
size_t out_len = 0;
if (EVP_PKEY_verify_recover(ctx, nullptr, &out_len, reinterpret_cast<const unsigned char *>(chunk.data()), chunk.size()) <= 0) {
qDebug() << "Failed to determine decrypted data size.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
QByteArray decrypted;
decrypted.resize(out_len);
if (EVP_PKEY_verify_recover(ctx, reinterpret_cast<unsigned char *>(decrypted.data()), &out_len, reinterpret_cast<const unsigned char *>(chunk.data()), chunk.size()) <= 0) {
qDebug() << "Verification (decryption) failed.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
decrypted.resize(out_len);
ret.append(decrypted);
}
EVP_PKEY_CTX_free(ctx);
return ret;
}
QByteArray Crypt::rsaEncrypt(const QByteArray &data, const QByteArray &privateKey)
{
EVP_PKEY *pkey = loadKeyFromDER(privateKey, false);
if (!pkey) {
return QByteArray();
}
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
if (!ctx) {
qDebug() << "Failed to create EVP_PKEY_CTX for encryption.";
EVP_PKEY_free(pkey);
ERR_print_errors_fp(stderr);
return QByteArray();
}
const int keySize =
#if OPENSSL_VERSION_NUMBER < 0x30000000L
EVP_PKEY_size(pkey);
#else
EVP_PKEY_get_size(pkey);
#endif
const int chunkSize = keySize - RSA_PKCS1_PADDING_SIZE;
EVP_PKEY_free(pkey);
if (chunkSize <= 0) {
qDebug() << "Invalid RSA key size for PKCS1 padding.";
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
if (EVP_PKEY_sign_init(ctx) <= 0) {
qDebug() << "Failed to initialize signing context.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) {
qDebug() << "Failed to set RSA padding.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
QByteArray ret;
for (int i = 0; i < ceil(data.size() * 1. / chunkSize); ++i) {
int offset = i * chunkSize;
QByteArray chunk = data.mid(offset, chunkSize);
size_t out_len = 0;
if (EVP_PKEY_sign(ctx, nullptr, &out_len, reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()) <= 0) {
qDebug() << "Failed to determine signature size.";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
QByteArray encryptedChunk(out_len, 0);
if (EVP_PKEY_sign(ctx, reinterpret_cast<unsigned char *>(encryptedChunk.data()), &out_len, reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()) <= 0) {
qDebug() << "Signing (encryption) failed.\n";
ERR_print_errors_fp(stderr);
EVP_PKEY_CTX_free(ctx);
return QByteArray();
}
encryptedChunk.resize(out_len);
ret.append(encryptedChunk);
}
EVP_PKEY_CTX_free(ctx);
return ret;
}
QByteArray Crypt::encryptToByteArray(const QByteArray &plaintext, const QByteArray &publicKey, const QByteArray &aesKey)
{
EncryptedData ed = encrypt(plaintext, publicKey, aesKey);
if (ed.isEmpty())
return QByteArray();
QByteArray ret;
QDataStream ds(&ret, QIODevice::WriteOnly);
ds << ed;
return ret;
}
EVP_PKEY *Crypt::loadKeyFromDER(const QByteArray &derKey, bool isPublicKey) {
// Create a memory BIO from the QByteArray
BIO *bio = BIO_new_mem_buf(derKey.constData(), derKey.size());
if (!bio) {
qWarning("Failed to create BIO");
return nullptr;
}
// Load the key based on whether it's a public or private key
EVP_PKEY *key = isPublicKey ? d2i_PUBKEY_bio(bio, nullptr) : d2i_PrivateKey_bio(bio, nullptr);
BIO_free(bio); // Free the BIO after loading
if (!key) {
qDebug() << "Failed to load key. SSL error:";
ERR_print_errors_fp(stderr);
}
return key;
}
QDataStream &operator<< (QDataStream &ds, const Crypt::EncryptedData &data)
{
ds << data.encryptedData;
ds << data.iv;
ds << data.encryptedKey;
ds << data.tag;
return ds;
}
QDataStream &operator>> (QDataStream &ds, Crypt::EncryptedData &data)
{
ds >> data.encryptedData;
ds >> data.iv;
ds >> data.encryptedKey;
ds >> data.tag;
return ds;
}

View File

@@ -0,0 +1,41 @@
#ifndef CRYPT_H
#define CRYPT_H
#include <openssl/evp.h>
#include <QByteArray>
#include <QPair>
class Crypt
{
static bool encryptAESKeyWithRSA(EVP_PKEY *publicKey, const unsigned char *aesKey, size_t aesKeyLength, QByteArray &encryptedKey);
static bool decryptAESKeyWithRSA(EVP_PKEY *privateKey, const QByteArray &encryptedKey, unsigned char *aesKey, size_t aesKeyLength);
static EVP_PKEY *loadKeyFromDER(const QByteArray &derPrivateKey, bool isPublicKey);
public:
struct EncryptedData {
QByteArray encryptedData;
QByteArray iv;
QByteArray encryptedKey;
QByteArray tag;
int size() { return encryptedData.size() + iv.size() + encryptedKey.size() + tag.size(); }
bool isEmpty() { return encryptedData.isEmpty(); }
};
static QPair<QByteArray, QByteArray> generateRSAKeyPair();
static QByteArray generateAESKey();
static EncryptedData encrypt(const QByteArray &plaintext, const QByteArray &publicKey, const QByteArray &aesKey);
static QByteArray encryptToByteArray(const QByteArray &plaintext, const QByteArray &publicKey, const QByteArray &aesKey);
static QByteArray decrypt(const EncryptedData &encryptedData, const QByteArray &privateKey);
static QByteArray decrypt(const QByteArray &encryptedData, const QByteArray &privateKey);
static bool isValidPublicKeyDER(const QByteArray& derKeyData);
static QByteArray rsaDecrypt(const QByteArray &encryptedData, const QByteArray &publicKey);
static QByteArray rsaEncrypt(const QByteArray &data, const QByteArray &privateKey);
};
QDataStream &operator<< (QDataStream &ds, const Crypt::EncryptedData &data);
QDataStream &operator>> (QDataStream &ds, Crypt::EncryptedData &data);
#endif // CRYPT_H

View File

@@ -13,7 +13,7 @@
// Self
#include "../def.h"
const static int COLUMN_COUNT = 8;
const static int COLUMN_COUNT = 9;
LicenseModel::LicenseModel(QObject* parent)
: QAbstractItemModel(parent)
@@ -59,6 +59,8 @@ QVariant LicenseModel::data(const QModelIndex &index, int role) const
case 6:
return m_data[index.row()].city ;
case 7:
return m_data[index.row()].licenseTime.isEmpty() ? tr("Permanent") : m_data[index.row()].licenseTime ;
case 8:
return m_data[index.row()].comment ;
default:
return {};
@@ -90,6 +92,8 @@ QVariant LicenseModel::headerData(int section, Qt::Orientation orientation, int
case 6:
return tr("City");
case 7:
return tr("License time");
case 8:
return tr("Comment");
default:
return {};
@@ -220,6 +224,7 @@ void LicenseModel::addClient(const LicenseItem &item)
m_status = Status::DbStructError;
m_errors.append("Inserted client not found");
emit statusChanged();
qDebug() << m_errors;
watcher->deleteLater();
return;
}
@@ -250,8 +255,8 @@ void LicenseModel::addClient(const LicenseItem &item)
{
QSqlQuery queryInsert(db);
queryInsert.prepare(QString(
"INSERT INTO clients(lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, comment) "
"VALUES (:lastName, :firstName, :patronymic, :phone, :email, :city, :yourCompany, :hardwareHash, :comment);"
"INSERT INTO clients(lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, licenseTime, comment) "
"VALUES (:lastName, :firstName, :patronymic, :phone, :email, :city, :yourCompany, :hardwareHash, :licenseTime, :comment);"
));
queryInsert.bindValue(":lastName", item.lastName);
queryInsert.bindValue(":firstName", item.firstName);
@@ -261,12 +266,14 @@ void LicenseModel::addClient(const LicenseItem &item)
queryInsert.bindValue(":city", item.city);
queryInsert.bindValue(":yourCompany", item.yourCompany);
queryInsert.bindValue(":hardwareHash", item.hardwareHash);
queryInsert.bindValue(":licenseTime", item.licenseTime);
queryInsert.bindValue(":comment", item.comment);
if (!queryInsert.exec())
{
result.status = LicenseModel::Status::DbStructError;
result.error = queryInsert.lastError().text();
qDebug() << result.error;
}
else
{
@@ -298,10 +305,8 @@ void LicenseModel::addClient(const LicenseItem &item)
output.city = querySelect.value("city").toString();
output.yourCompany = querySelect.value("yourCompany").toString();
output.hardwareHash = querySelect.value("hardwareHash").toString();
output.licenseTime = querySelect.value("licenseTime").toString();
output.comment = querySelect.value("comment").toString();
const int createdAtUtcIndex = querySelect.record().indexOf("createdAtUtc");
if (createdAtUtcIndex >= 0)
output.createdAtUtc = querySelect.value(createdAtUtcIndex).toString();
result.data.append(output);
}
}
@@ -425,8 +430,6 @@ void LicenseModel::editClient(const LicenseItem &item, int index)
LicenseItem updateItem = item;
updateItem.id = m_data[index].id;
if (updateItem.createdAtUtc.isEmpty())
updateItem.createdAtUtc = m_data[index].createdAtUtc;
if (updateItem.id.isEmpty())
{
@@ -491,7 +494,7 @@ void LicenseModel::editClient(const LicenseItem &item, int index)
queryUpdate.prepare(QString(
"UPDATE clients SET lastName=:lastName, firstName=:firstName, patronymic=:patronymic, "
"phone=:phone, email=:email, city=:city, yourCompany=:yourCompany, hardwareHash=:hardwareHash, "
"comment=:comment WHERE id=:id;"
"licenseTime=:licenseTime, comment=:comment WHERE id=:id;"
));
queryUpdate.bindValue(":lastName", updateItem.lastName);
queryUpdate.bindValue(":firstName", updateItem.firstName);
@@ -501,6 +504,7 @@ void LicenseModel::editClient(const LicenseItem &item, int index)
queryUpdate.bindValue(":city", updateItem.city);
queryUpdate.bindValue(":yourCompany", updateItem.yourCompany);
queryUpdate.bindValue(":hardwareHash", updateItem.hardwareHash);
queryUpdate.bindValue(":licenseTime", updateItem.licenseTime);
queryUpdate.bindValue(":comment", updateItem.comment);
queryUpdate.bindValue(":id", updateItem.id);
@@ -537,12 +541,8 @@ void LicenseModel::editClient(const LicenseItem &item, int index)
output.city = querySelect.value("city").toString();
output.yourCompany = querySelect.value("yourCompany").toString();
output.hardwareHash = querySelect.value("hardwareHash").toString();
output.licenseTime = querySelect.value("licenseTime").toString();
output.comment = querySelect.value("comment").toString();
const int createdAtUtcIndex = querySelect.record().indexOf("createdAtUtc");
if (createdAtUtcIndex >= 0)
output.createdAtUtc = querySelect.value(createdAtUtcIndex).toString();
else
output.createdAtUtc = updateItem.createdAtUtc;
result.data.append(output);
}
}
@@ -596,7 +596,7 @@ void LicenseModel::updateModel()
{
QSqlQuery query(db);
if (!query.exec(
"SELECT id, lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, comment "
"SELECT id, lastName, firstName, patronymic, phone, email, city, yourCompany, hardwareHash, licenseTime, comment "
"FROM clients ORDER BY id DESC;"
))
{
@@ -605,7 +605,6 @@ void LicenseModel::updateModel()
}
else
{
const int createdAtUtcIndex = query.record().indexOf("createdAtUtc");
while (query.next())
{
LicenseItem item;
@@ -618,9 +617,8 @@ void LicenseModel::updateModel()
item.city = query.value("city").toString();
item.yourCompany = query.value("yourCompany").toString();
item.hardwareHash = query.value("hardwareHash").toString();
item.licenseTime = query.value("licenseTime").toString();
item.comment = query.value("comment").toString();
if (createdAtUtcIndex >= 0)
item.createdAtUtc = query.value(createdAtUtcIndex).toString();
result.data.append(item);
}
}

View File

@@ -31,7 +31,7 @@ public:
QString patronymic;
QString phone;
QString yourCompany;
QString createdAtUtc;
QString licenseTime;
QString comment;
};

View File

@@ -3,13 +3,14 @@
// Qt
#include <QApplication>
#include <QDebug>
#include <QFileDialog>
#include <QHeaderView>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSettings>
#include <QStatusBar>
#include <QStandardPaths>
#include <QTableView>
#include <QTimer>
#include <QToolBar>
@@ -17,6 +18,8 @@
// Self
#include "../def.h"
#include "LicenseGenerator/crypt.h"
#include "LicenseGenerator/Util.h"
#include "LicenseModel/LicenseModel.h"
#include "EditClientDialog/EditClientDialog.h"
@@ -44,18 +47,28 @@ MainWidget::MainWidget(QWidget *parent)
{
if (m_licenseModel->getStatus() == LicenseModel::Status::DbStructError)
{
if (!m_licenseModel->prepareDatabase())
{
QTimer::singleShot(0, [&]() {
QMessageBox messageBox;
messageBox.setIcon(QMessageBox::Critical);
messageBox.setWindowTitle(tr("Error"));
messageBox.setText(tr("Error with prepare database"));
messageBox.setDetailedText(m_licenseModel->getStatusText());
messageBox.exec();
QTimer::singleShot(0, [&]() {
auto result = QMessageBox::question(
this
, tr("Database not exist")
, tr("Create new database?")
);
if (result == QMessageBox::Button::Yes)
{
if (!m_licenseModel->prepareDatabase())
{
QMessageBox::critical(
this
, tr("Error")
, tr("Database create error")
);
}
}
else if (result == QMessageBox::Button::No)
QApplication::quit();
});
}
else
qDebug() << "result" << result;
});
}
}
@@ -94,11 +107,14 @@ MainWidget::MainWidget(QWidget *parent)
m_addClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/add.png")), tr("Add client"));
m_editClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/edit.png")), tr("Edit client"));
m_deleteClientsMenuAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/delete.png")), tr("Delete client"));
m_menu->addSeparator();
m_getLicenseFileAction = m_menu->addAction(QIcon(QStringLiteral(":/deps/getLicenseFile.png")), tr("Get license file"));
connect(m_reloadTableMenuAction, &QAction::triggered, this, &MainWidget::onReloadTableTriggered);
connect(m_addClientsMenuAction, &QAction::triggered, this, &MainWidget::onAddClientTriggered);
connect(m_editClientsMenuAction, &QAction::triggered, this, &MainWidget::onEditClientTriggered);
connect(m_deleteClientsMenuAction, &QAction::triggered, this, &MainWidget::onDeleteClientTriggered);
connect(m_getLicenseFileAction, &QAction::triggered, this, &MainWidget::onGetLicenseFileTriggered);
m_tableView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_tableView, &QTableView::customContextMenuRequested,
@@ -199,6 +215,88 @@ void MainWidget::onDeleteClientTriggered()
m_licenseModel->deleteClient(list);
}
void MainWidget::onGetLicenseFileTriggered()
{
auto selectedRows = m_tableView->selectionModel()->selectedRows();
if (selectedRows.size() != 1)
return;
const int row = selectedRows.first().row();
const auto item = m_licenseModel->getItem(row);
QStringList nameParts;
if (!item.lastName.isEmpty())
nameParts << item.lastName;
if (!item.firstName.isEmpty())
nameParts << item.firstName;
if (!item.patronymic.isEmpty())
nameParts << item.patronymic;
if (nameParts.isEmpty() && !item.yourCompany.isEmpty())
nameParts << item.yourCompany;
QString baseName = nameParts.join('_');
if (baseName.isEmpty())
baseName = "license";
baseName.replace('/', '_');
baseName.replace('\\', '_');
QString baseDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
if (baseDir.isEmpty())
baseDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
const QString defaultPath = baseDir + "/" + baseName + ".dat";
QString filePath = QFileDialog::getSaveFileName(
this,
tr("Save license file"),
defaultPath,
tr("License files (*.license);;All files (*)")
);
if (filePath.isEmpty())
return;
if (!filePath.endsWith(".dat", Qt::CaseInsensitive))
filePath += ".dat";
QJsonObject licenseObject;
licenseObject.insert("machineId", item.hardwareHash);
if (!item.licenseTime.isEmpty())
licenseObject.insert("validUntil", QDate::fromString(item.licenseTime).toString(Qt::ISODate));
QJsonDocument doc(licenseObject);
const QByteArray data = doc.toJson(QJsonDocument::Indented);
const QByteArray privateKey = fileReadAll(":/deps/Key");
if (privateKey.isEmpty())
{
QMessageBox::warning(
this,
tr("Error"),
tr("Failed to load encryption key")
);
return;
}
QByteArray encrypted = Crypt::rsaEncrypt(toCbor(doc), privateKey);
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
QMessageBox::warning(
this,
tr("Error"),
tr("Failed to open license file: %1").arg(file.errorString())
);
return;
}
if (file.write(encrypted.toHex()) != encrypted.toHex().size())
{
QMessageBox::warning(
this,
tr("Error"),
tr("Failed to write license file: %1").arg(file.errorString())
);
}
}
void MainWidget::selectionChanged()
{
auto selectedCount = m_tableView->selectionModel()->selectedRows().size();
@@ -208,6 +306,7 @@ void MainWidget::selectionChanged()
m_addClientsMenuAction->setEnabled(m_licenseModel->getStatus() == LicenseModel::Status::Ok);
m_editClientsMenuAction->setEnabled(selectedCount == 1 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
m_deleteClientsMenuAction->setEnabled(selectedCount > 0 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
m_getLicenseFileAction->setEnabled(selectedCount == 1 && m_licenseModel->getStatus() == LicenseModel::Status::Ok);
}
void MainWidget::closeEvent(QCloseEvent *event)
@@ -282,4 +381,3 @@ void MainWidget::modelStatusChanged()
selectionChanged();
}

View File

@@ -31,6 +31,7 @@ private slots:
void onAddClientTriggered();
void onEditClientTriggered();
void onDeleteClientTriggered();
void onGetLicenseFileTriggered();
void onEditClientDialogClosed(int result);
void selectionChanged();
void modelStatusChanged();
@@ -51,6 +52,7 @@ private:
QAction* m_addClientsMenuAction{nullptr};
QAction* m_deleteClientsMenuAction{nullptr};
QAction* m_editClientsMenuAction{nullptr};
QAction* m_getLicenseFileAction{nullptr};
EditClientDialog* m_editClientDialog{nullptr};
QStatusBar* m_statusBar{nullptr};
QLabel* m_statusLabel{nullptr};