Hardware-locked license key"

Good pair of questions — this is a classic “hardware-locked license key” system plus LAN discovery. Let me walk through both.

1. Hardware fingerprinting + license key system

Step 1: Collect hardware identifiers (Windows, C++)

The most stable identifiers are CPU ID, motherboard serial, disk serial, and BIOS UUID. Use WMI (Wbemuidl) since it’s the standard, maintained way to query these on Windows.

#include <windows.h>
#include <comdef.h>
#include <Wbemidl.h>
#include <string>
#pragma comment(lib, "wbemuuid.lib")

std::wstring QueryWmi(const std::wstring& wmiClass, const std::wstring& property) {
    std::wstring result;
    HRESULT hres;

    hres = CoInitializeEx(0, COINIT_MULTITHREADED);
    hres = CoInitializeSecurity(NULL, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);

    IWbemLocator* pLoc = nullptr;
    CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, (LPVOID*)&pLoc);

    IWbemServices* pSvc = nullptr;
    pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0,
        NULL, 0, 0, &pSvc);

    CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
        RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE);

    IEnumWbemClassObject* pEnumerator = nullptr;
    std::wstring query = L"SELECT " + property + L" FROM " + wmiClass;
    pSvc->ExecQuery(_bstr_t(L"WQL"), _bstr_t(query.c_str()),
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &pEnumerator);

    IWbemClassObject* pclsObj = nullptr;
    ULONG uReturn = 0;
    if (pEnumerator) {
        pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);
        if (uReturn > 0) {
            VARIANT vtProp;
            pclsObj->Get(property.c_str(), 0, &vtProp, 0, 0);
            if (vtProp.vt == VT_BSTR) result = vtProp.bstrVal;
            VariantClear(&vtProp);
            pclsObj->Release();
        }
        pEnumerator->Release();
    }

    pSvc->Release();
    pLoc->Release();
    CoUninitialize();
    return result;
}

// Usage:
// std::wstring cpuId    = QueryWmi(L"Win32_Processor", L"ProcessorId");
// std::wstring boardSN  = QueryWmi(L"Win32_BaseBoard", L"SerialNumber");
// std::wstring diskSN   = QueryWmi(L"Win32_DiskDrive", L"SerialNumber");
// std::wstring biosUUID = QueryWmi(L"Win32_ComputerSystemProduct", L"UUID");

Step 2: Derive a stable machine ID

Concatenate the fields and hash them (SHA-256 via Qt’s QCryptographicHash, or OpenSSL):

#include <QCryptographicHash>
#include <QString>

QString GenerateMachineId(const QString& cpuId, const QString& boardSN,
                           const QString& diskSN, const QString& biosUUID) {
    QString combined = cpuId + "|" + boardSN + "|" + diskSN + "|" + biosUUID;
    QByteArray hash = QCryptographicHash::hash(combined.toUtf8(), QCryptographicHash::Sha256);
    return hash.toHex().toUpper(); // 64-char hex machine ID
}

Practical notes:

  • Some fields can be blank/spoofed on certain hardware (VMs, cheap boards report empty serials) — pick 2–3 fields and treat missing ones gracefully rather than failing.
  • Truncate/format the hash into a friendlier “Machine ID: XXXX-XXXX-XXXX-XXXX” for display if the user needs to email it to you.

Step 3: Key generator design

The standard, secure pattern is asymmetric signing — not a shared-secret hash both sides can compute, since that can be reverse-engineered out of the client binary. The idea: the client generates a machine ID and shows it to the user; the vendor signs machineId + licenseInfo with a private key kept only on the vendor side; the client verifies the signature with an embedded public key.

Vendor-side generator (separate small Qt6 tool, keeps the private key):

#include <QByteArray>
#include <openssl/pem.h>
#include <openssl/evp.h>

QByteArray SignLicense(const QString& machineId, const QString& licenseData, EVP_PKEY* privateKey) {
    QByteArray payload = (machineId + "|" + licenseData).toUtf8();

    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestSignInit(ctx, nullptr, EVP_sha256(), nullptr, privateKey);
    EVP_DigestSignUpdate(ctx, payload.constData(), payload.size());

    size_t sigLen = 0;
    EVP_DigestSignFinal(ctx, nullptr, &sigLen);
    QByteArray signature(sigLen, 0);
    EVP_DigestSignFinal(ctx, reinterpret_cast<unsigned char*>(signature.data()), &sigLen);
    EVP_MD_CTX_free(ctx);

    // Final key = base64(payload) + "." + base64(signature)
    return payload.toBase64() + "." + signature.left(sigLen).toBase64();
}

Client-side verification (public key embedded in the binary):

bool VerifyLicenseKey(const QString& licenseKey, const QString& expectedMachineId, EVP_PKEY* publicKey) {
    QStringList parts = licenseKey.split('.');
    if (parts.size() != 2) return false;

    QByteArray payload = QByteArray::fromBase64(parts[0].toUtf8());
    QByteArray signature = QByteArray::fromBase64(parts[1].toUtf8());

    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, publicKey);
    EVP_DigestVerifyUpdate(ctx, payload.constData(), payload.size());
    bool valid = (EVP_DigestVerifyFinal(ctx,
        reinterpret_cast<const unsigned char*>(signature.constData()),
        signature.size()) == 1);
    EVP_MD_CTX_free(ctx);
    if (!valid) return false;

    // Confirm the payload's machine ID actually matches this PC
    QString payloadStr = QString::fromUtf8(payload);
    QString embeddedId = payloadStr.split('|').first();
    return embeddedId == expectedMachineId;
}

Step 4: Client registration flow

class RegistrationManager {
public:
    bool isRegistered() {
        QSettings settings;
        QString storedKey = settings.value("license/key").toString();
        QString machineId = GenerateMachineId(/* gathered fields */);
        return VerifyLicenseKey(storedKey, machineId, m_publicKey);
    }

    bool registerWithKey(const QString& key) {
        QString machineId = GenerateMachineId(/* gathered fields */);
        if (VerifyLicenseKey(key, machineId, m_publicKey)) {
            QSettings settings;
            settings.setValue("license/key", key);
            return true;
        }
        return false;
    }
private:
    EVP_PKEY* m_publicKey; // loaded from embedded PEM at startup
};

Typical UX: on first launch, show the machine ID for the user to send you (or auto-request via your license server’s API), then present a dialog for pasting the returned key, calling registerWithKey().

2. Broadcasting PC name + IPv4 over LAN with Qt6

QUdpSocket is the standard tool — broadcast a small UDP datagram periodically, and have peers listen on the same port.

Broadcaster:

#include <QUdpSocket>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QTimer>
#include <QJsonObject>
#include <QJsonDocument>

class LanBroadcaster : public QObject {
    Q_OBJECT
public:
    LanBroadcaster(quint16 port, QObject* parent = nullptr) : QObject(parent), m_port(port) {
        m_socket = new QUdpSocket(this);
        m_timer = new QTimer(this);
        connect(m_timer, &QTimer::timeout, this, &LanBroadcaster::broadcast);
        m_timer->start(2000); // every 2 seconds
    }

private slots:
    void broadcast() {
        QString hostname = QHostInfo::localHostName();
        QString ip = getLocalIPv4();

        QJsonObject obj{{"name", hostname}, {"ip", ip}};
        QByteArray datagram = QJsonDocument(obj).toJson(QJsonDocument::Compact);

        m_socket->writeDatagram(datagram, QHostAddress::Broadcast, m_port);
    }

    QString getLocalIPv4() {
        for (const QHostAddress& addr : QNetworkInterface::allAddresses()) {
            if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback())
                return addr.toString();
        }
        return QString();
    }

private:
    QUdpSocket* m_socket;
    QTimer* m_timer;
    quint16 m_port;
};

Listener (on other machines):

class LanListener : public QObject {
    Q_OBJECT
public:
    LanListener(quint16 port, QObject* parent = nullptr) : QObject(parent) {
        m_socket = new QUdpSocket(this);
        m_socket->bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint);
        connect(m_socket, &QUdpSocket::readyRead, this, &LanListener::readPending);
    }

signals:
    void peerDiscovered(const QString& name, const QString& ip);

private slots:
    void readPending() {
        while (m_socket->hasPendingDatagrams()) {
            QByteArray buffer;
            buffer.resize(int(m_socket->pendingDatagramSize()));
            QHostAddress sender;
            m_socket->readDatagram(buffer.data(), buffer.size(), &sender);

            QJsonDocument doc = QJsonDocument::fromJson(buffer);
            QJsonObject obj = doc.object();
            emit peerDiscovered(obj["name"].toString(), obj["ip"].toString());
        }
    }

private:
    QUdpSocket* m_socket;
};

Notes:

  • Broadcast UDP only reaches devices on the same subnet/broadcast domain — it won’t cross routers, which is usually what you want for LAN discovery.
  • Pick a port above 1024 to avoid needing elevated privileges, and avoid well-known ports already in use.
  • If you need discovery to survive on more complex networks (VLANs, Wi-Fi client isolation), consider mDNS/Bonjour (QZeroConf or similar) as a more robust alternative to raw broadcast.

Let me know if you want the OpenSSL key-pair generation commands for signing, or a Qt6 dialog UI for the registration flow.