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};