diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0d32ed5..99b53eb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
diff --git a/deps/Key b/deps/Key
new file mode 100644
index 0000000..0f0c982
Binary files /dev/null and b/deps/Key differ
diff --git a/deps/deps.qrc b/deps/deps.qrc
index 29a02b0..8cdac45 100644
--- a/deps/deps.qrc
+++ b/deps/deps.qrc
@@ -8,6 +8,8 @@
delete.png
info.png
dropFiles.png
+ getLicenseFile.png
tables.ddl
+ Key
\ No newline at end of file
diff --git a/deps/getLicenseFile.png b/deps/getLicenseFile.png
new file mode 100644
index 0000000..b87c0e2
Binary files /dev/null and b/deps/getLicenseFile.png differ
diff --git a/deps/tables.ddl b/deps/tables.ddl
index e89bed5..3ee0c0d 100644
--- a/deps/tables.ddl
+++ b/deps/tables.ddl
@@ -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
);
diff --git a/src/EditClientDialog/EditClientDialog.cpp b/src/EditClientDialog/EditClientDialog.cpp
index 0085e00..6971915 100644
--- a/src/EditClientDialog/EditClientDialog.cpp
+++ b/src/EditClientDialog/EditClientDialog.cpp
@@ -1,6 +1,8 @@
#include "EditClientDialog.h"
// Qt
+#include
+#include
#include
#include
#include
@@ -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"));
}
diff --git a/src/EditClientDialog/EditClientDialog.h b/src/EditClientDialog/EditClientDialog.h
index 650b0e8..3586f56 100644
--- a/src/EditClientDialog/EditClientDialog.h
+++ b/src/EditClientDialog/EditClientDialog.h
@@ -2,12 +2,15 @@
#define LICENSEMANAGER_EDITCLIENTDIALOG_H
// Qt
+#include
#include
#include
#include
#include
#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;
};
diff --git a/src/LicenseGenerator/LicenseGenerator.cpp b/src/LicenseGenerator/LicenseGenerator.cpp
new file mode 100644
index 0000000..1751991
--- /dev/null
+++ b/src/LicenseGenerator/LicenseGenerator.cpp
@@ -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;
+}
diff --git a/src/LicenseGenerator/LicenseGenerator.h b/src/LicenseGenerator/LicenseGenerator.h
new file mode 100644
index 0000000..8d5e52b
--- /dev/null
+++ b/src/LicenseGenerator/LicenseGenerator.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+class LicenseGenerator
+{
+public:
+ enum class Error {
+ Ok,
+ SshFailed,
+ CpuIdEmpty,
+ CpuIdInvalid,
+ EncryptFailed,
+ WriteFailed
+ };
+
+ struct Result {
+ Error error = Error::Ok;
+ QByteArray cpuId;
+ };
+
+ using RunSshFn = std::function;
+
+ static Result createEncryptedFile(const QString& outputFile,
+ const QString& sshTarget,
+ const QString& password,
+ const RunSshFn& runSsh);
+};
diff --git a/src/LicenseGenerator/Util.cpp b/src/LicenseGenerator/Util.cpp
new file mode 100644
index 0000000..dff9bf8
--- /dev/null
+++ b/src/LicenseGenerator/Util.cpp
@@ -0,0 +1,410 @@
+#include "Util.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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& 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 splitToInts(const QString& s, QChar sep)
+{
+ QVector 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 &vector)
+{
+ QJsonArray resultArray;
+ for (const bool & value: vector){
+ resultArray.append(value);
+ }
+ return resultArray;
+}
+
+QVector convertJsonArrayToVector(const QJsonArray &array)
+{
+ QVector 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 &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 arrayFromMask(uint8_t mask)
+{
+ int idx=0;
+ QVector ret;
+ while (mask) {
+ if(mask & 1) ret.append(idx);
+ mask>>=1;
+ idx++;
+ }
+ return ret;
+}
+
+
+QVector intersectUnique(const QVector &a, const QVector &b) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ QSet sa(a.cbegin(), a.cend());
+ QSet sb(b.cbegin(), b.cend());
+#else
+ QSet sa = QSet::fromList(a.toList());
+ QSet sb = QSet::fromList(b.toList());
+#endif
+
+ // In-place intersection; 'sa' becomes the result
+ sa.intersect(sb);
+
+ QVector 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 &vec, bool value)
+{
+ int count = 0;
+ for (bool v : vec) {
+ if (v == value)
+ ++count;
+ }
+ return count;
+}
+
+QVector vectorOr(const QVector &v1, const QVector &v2)
+{
+ Q_ASSERT(v1.size() == v2.size()); // sanity check
+ QVector v3(v1.size());
+ std::transform(v1.cbegin(), v1.cend(), v2.cbegin(), v3.begin(),
+ [](bool x, bool y){ return x || y; });
+ return v3;
+}
+
+QVector invert(const QVector &v)
+{
+ QVector result(v.size());
+ std::transform(v.cbegin(), v.cend(), result.begin(),
+ [](bool b){ return !b; });
+ return result;
+}
+
+QString toString(const QVector &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(", ");
+}
diff --git a/src/LicenseGenerator/Util.h b/src/LicenseGenerator/Util.h
new file mode 100644
index 0000000..07d473d
--- /dev/null
+++ b/src/LicenseGenerator/Util.h
@@ -0,0 +1,304 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+template
+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 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(sum) / m_container.size();
+ }
+ void clear() { m_container.clear(); }
+ std::deque &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 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 >
+class Ptr : public QScopedPointer
+{
+ typedef QScopedPointer 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 Vector : public std::vector
+{
+ typedef std::vector 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 List : public std::list
+{
+ typedef std::list 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 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 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 inline bool contains(I first, I last, const T& value)
+{
+ I i = std::find(first, last, value);
+ return i != last;
+}
+template inline bool containsIf(I first, I last, P pred)
+{
+ I i = std::find_if(first, last, pred);
+ return i != last;
+}
+
+template inline C& sort(C& c) { std::sort(c.begin(), c.end()); return c; }
+template inline C& sort(C& c, const P& pred) { std::sort(c.begin(), c.end(), pred); return c; }
+
+template inline bool contains(const C& c, const typename C::value_type& value) { return contains(c.begin(), c.end(), value); }
+template inline bool containsIf(const C& c, P pred) { return containsIf(c.begin(), c.end(), pred); }
+
+template inline typename C::iterator find(C& c, const T& v) { return std::find(c.begin(), c.end(), v); }
+template inline typename C::const_iterator find(const C& c, const T& v) { return std::find(c.begin(), c.end(), v); }
+
+template inline typename C::iterator findIf(C& c, const P& pred) { return std::find_if(c.begin(), c.end(), pred); }
+template inline typename C::const_iterator findIf(const C& c, const P& pred) { return std::find_if(c.begin(), c.end(), pred); }
+template 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 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 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 C& forEach(C& c, F f) { std::for_each(c.begin(), c.end(), f); return c; }
+template const C& forEach(const C& c, F f) { std::for_each(c.begin(), c.end(), f); return c; }
+template inline void fill(C& c, const T& value) { std::fill(c.begin(), c.end(), value); }
+
+template It copy(C& c, It to) { return std::copy(c.begin(), c.end(), to); }
+
+//template C select(const C& v, P pred)
+
+template QVector selectIndexes(const QVector& v, P pred)
+{
+ QVector r;
+ for(int i = 0; i < int(v.size()); ++i)
+ if (pred(v[i]))
+ r << i;
+ return r;
+}
+
+template 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
+inline decltype(auto) get_if_type(Variant& v) {
+ return std::holds_alternative(v) ? std::addressof(std::get(v)) : nullptr;
+}
+
+template
+inline const T* get_if_polymorphic(const Variant& v) {
+ if (const auto* p = dynamic_cast(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& v, QChar sep);
+inline QString join(const QVector& v, QChar sep)
+{
+ QString s;
+ append(s, v, sep);
+ return s;
+}
+QString &append(QString &s, const QVector &v, QChar sep);
+QVector splitToInts(const QString& s, QChar sep);
+
+QVector getIpAddr();
+
+QVector convertStringToVectorBool(const QString &string);
+QByteArray addSizeToByteArray(const QByteArray &arr);
+int getSizeJsonInByteArr(const QByteArray &arr);
+QString toString(const QVector &vec, bool value);
+//Counts values in bool vectors
+int popCount(const QVector &vec, bool value);
+QVector vectorOr(const QVector &a, const QVector &b);
+QVector invert(const QVector &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 &vector);
+QVector 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 arrayFromMask(uint8_t mask);
+QVector intersectUnique(const QVector& a, const QVector& b);
diff --git a/src/LicenseGenerator/crypt.cpp b/src/LicenseGenerator/crypt.cpp
new file mode 100644
index 0000000..909dc2e
--- /dev/null
+++ b/src/LicenseGenerator/crypt.cpp
@@ -0,0 +1,478 @@
+#include "crypt.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+QPair Crypt::generateRSAKeyPair() {
+ QPair 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(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(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(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(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(aesKey.data()), reinterpret_cast(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(ciphertext.data()), &len, reinterpret_cast(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(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(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(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(decryptedText.data()), &len,
+ reinterpret_cast(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(reinterpret_cast(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(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, it’s 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(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(decrypted.data()), &out_len, reinterpret_cast(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(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(encryptedChunk.data()), &out_len, reinterpret_cast(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;
+}
diff --git a/src/LicenseGenerator/crypt.h b/src/LicenseGenerator/crypt.h
new file mode 100644
index 0000000..8333123
--- /dev/null
+++ b/src/LicenseGenerator/crypt.h
@@ -0,0 +1,41 @@
+#ifndef CRYPT_H
+#define CRYPT_H
+
+#include
+
+#include
+#include
+
+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 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
diff --git a/src/LicenseModel/LicenseModel.cpp b/src/LicenseModel/LicenseModel.cpp
index 541e3b5..6fa1e2c 100644
--- a/src/LicenseModel/LicenseModel.cpp
+++ b/src/LicenseModel/LicenseModel.cpp
@@ -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);
}
}
diff --git a/src/LicenseModel/LicenseModel.h b/src/LicenseModel/LicenseModel.h
index f29f0dd..b48fc23 100644
--- a/src/LicenseModel/LicenseModel.h
+++ b/src/LicenseModel/LicenseModel.h
@@ -31,7 +31,7 @@ public:
QString patronymic;
QString phone;
QString yourCompany;
- QString createdAtUtc;
+ QString licenseTime;
QString comment;
};
diff --git a/src/MainWidget/MainWidget.cpp b/src/MainWidget/MainWidget.cpp
index 223dfb2..bdab7e0 100644
--- a/src/MainWidget/MainWidget.cpp
+++ b/src/MainWidget/MainWidget.cpp
@@ -3,13 +3,14 @@
// Qt
#include
#include
+#include
#include
-#include
#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -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();
}
-
diff --git a/src/MainWidget/MainWidget.h b/src/MainWidget/MainWidget.h
index dcb9151..d1cf6c0 100644
--- a/src/MainWidget/MainWidget.h
+++ b/src/MainWidget/MainWidget.h
@@ -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};