Index: barcode/README.md =================================================================== diff -u --- barcode/README.md (revision 0) +++ barcode/README.md (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,28 @@ +# Diality Secure DataMatrix (Qt 5.15 + qmake + OpenSSL) — TLV + Auto-fit + +Applied improvements: +- Payload redesigned as compact **TLV** (UTF-8 strings) +- Optional **compress-before-encrypt** (qCompress) when it actually reduces size +- PC tool auto-computes **pixels/module** to fit a target size (default 14.4mm) at a given printer DPI (default 300) +- Quiet zone reduced to **1 module** + +## PC input format +Enter fields like: +- PID=CARTRIDGE-123|LOT=A24X7|EXP=20271231|SN=0000009876543210 + +EXP can also be YYMMDD (e.g., 271231). + +## keys.json locations +- PC tool: `/keys.json` +- Device reader: `/etc/diality/keys.json` (fallback `/keys.json`) + +## Dependencies +- Qt 5.15 (Widgets) +- OpenSSL (libssl + libcrypto) +- ZXing-C++ (PC tool only) + +## qmake variables +- OPENSSL_INCLUDE_DIR +- OPENSSL_LIB_DIR +- ZXING_INCLUDE_DIR +- ZXING_LIB_DIR Index: barcode/common/diality_keys.cpp =================================================================== diff -u --- barcode/common/diality_keys.cpp (revision 0) +++ barcode/common/diality_keys.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,121 @@ +#include "diality_keys.h" + +#include +#include +#include +#include +#include + +static bool parseHexField(const QJsonObject& obj, const char* name, int expectedBytes, QByteArray* out, QString* err) +{ + if (!obj.contains(name) || !obj.value(name).isString()) { + if (err) *err = QString("Missing or invalid field: %1").arg(QString::fromLatin1(name)); + return false; + } + const QString s = obj.value(name).toString().trimmed(); + const QString h = QString(s).remove(' '); + if (h.size() != expectedBytes * 2) { + if (err) *err = QString("Field %1: wrong hex length (expected %2 hex chars)") + .arg(QString::fromLatin1(name)) + .arg(expectedBytes * 2); + return false; + } + const QByteArray bin = QByteArray::fromHex(h.toLatin1()); + if (bin.size() != expectedBytes) { + if (err) *err = QString("Field %1: hex decode failed").arg(QString::fromLatin1(name)); + return false; + } + if (out) *out = bin; + return true; +} + +namespace diality { + +bool loadKeysFromJsonFile(const QString& path, Keys* outKeys, QString* error) +{ + if (!outKeys) { + if (error) *error = "Internal error: outKeys is null"; + return false; + } + + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + if (error) *error = QString("Cannot open keys file: %1").arg(path); + return false; + } + + const QByteArray data = f.readAll(); + f.close(); + + QJsonParseError pe; + const QJsonDocument doc = QJsonDocument::fromJson(data, &pe); + if (doc.isNull() || !doc.isObject()) { + if (error) *error = QString("Invalid JSON in %1: %2").arg(path, pe.errorString()); + return false; + } + + const QJsonObject obj = doc.object(); + + QString err; + Keys k; + + if (!parseHexField(obj, "encKey32", 32, &k.encKey32, &err)) { if (error) *error = err; return false; } + if (obj.contains("signSk32")) { + if (!parseHexField(obj, "signSk32", 32, &k.signSk32, &err)) { if (error) *error = err; return false; } + } + if (!parseHexField(obj, "signPk32", 32, &k.signPk32, &err)) { if (error) *error = err; return false; } + + *outKeys = k; + return true; +} + +static QString exeDirKeysJson() +{ + const QString exe = QCoreApplication::applicationFilePath(); + const QFileInfo fi(exe); + return fi.absolutePath() + "/keys.json"; +} + +bool loadPcKeys(Keys* outKeys, QString* loadedPath, QString* error) +{ + const QString p = exeDirKeysJson(); + Keys k; + QString err; + if (!loadKeysFromJsonFile(p, &k, &err)) { + if (error) *error = err; + return false; + } + if (k.signSk32.size() != 32) { + if (error) *error = "keys.json is missing signSk32 (PC needs private key to sign)"; + return false; + } + *outKeys = k; + if (loadedPath) *loadedPath = p; + return true; +} + +bool loadDeviceKeys(Keys* outKeys, QString* loadedPath, QString* error) +{ + const QStringList candidates = { + "/etc/diality/keys.json", + exeDirKeysJson() + }; + + QString lastErr; + for (const QString& p : candidates) { + Keys k; + QString err; + if (loadKeysFromJsonFile(p, &k, &err)) { + k.signSk32.clear(); + *outKeys = k; + if (loadedPath) *loadedPath = p; + return true; + } + lastErr = err; + } + + if (error) *error = lastErr.isEmpty() ? "No keys file found" : lastErr; + return false; +} + +} // namespace diality Index: barcode/common/diality_keys.h =================================================================== diff -u --- barcode/common/diality_keys.h (revision 0) +++ barcode/common/diality_keys.h (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace diality { + +struct Keys { + QByteArray encKey32; + QByteArray signSk32; // PC only + QByteArray signPk32; // device +}; + +bool loadKeysFromJsonFile(const QString& path, Keys* outKeys, QString* error); +bool loadPcKeys(Keys* outKeys, QString* loadedPath, QString* error); +bool loadDeviceKeys(Keys* outKeys, QString* loadedPath, QString* error); + +} // namespace diality Index: barcode/common/diality_payload.cpp =================================================================== diff -u --- barcode/common/diality_payload.cpp (revision 0) +++ barcode/common/diality_payload.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,186 @@ +#include "diality_payload.h" + +#include +#include + +namespace { + +static void tlvAppend(QByteArray& out, quint8 type, const QByteArray& value) +{ + const int len = value.size(); + if (len < 0 || len > 255) return; // caller ensures bounds + out.append(char(type)); + out.append(char(len)); + out.append(value); +} + +static bool isReasonableAscii(const QString& s) +{ + // Keep this permissive: allow printable ASCII plus a few common separators. + // You can tighten this later if you want. + for (const QChar& ch : s) { + const ushort u = ch.unicode(); + if (u < 0x20 || u > 0x7E) return false; + } + return true; +} + +} // namespace + +namespace diality { + +bool parseKeyValueInput( + const QString& input, + QString* outPartId, + QString* outLot, + QString* outExp, + QString* outSerial, + QString* outError +) +{ + if (outPartId) outPartId->clear(); + if (outLot) outLot->clear(); + if (outExp) outExp->clear(); + if (outSerial) outSerial->clear(); + + const QString trimmed = input.trimmed(); + if (trimmed.isEmpty()) { if (outError) *outError = "Input is empty"; return false; } + + const QStringList pairs = trimmed.split('|', Qt::SkipEmptyParts); + for (const QString& p : pairs) { + const int eq = p.indexOf('='); + if (eq <= 0) continue; + + const QString k = p.left(eq).trimmed().toUpper(); + const QString v = p.mid(eq + 1).trimmed(); + + if (k == "PID" || k == "PARTID") { + if (outPartId) *outPartId = v; + } else if (k == "LOT") { + if (outLot) *outLot = v; + } else if (k == "EXP") { + if (outExp) *outExp = v; + } else if (k == "SN" || k == "SERIAL") { + if (outSerial) *outSerial = v; + } + } + + if (!outPartId || outPartId->isEmpty()) { if (outError) *outError = "Missing PID=..."; return false; } + if (!outLot || outLot->isEmpty()) { if (outError) *outError = "Missing LOT=..."; return false; } + if (!outExp || outExp->isEmpty()) { if (outError) *outError = "Missing EXP=..."; return false; } + if (!outSerial || outSerial->isEmpty()) { if (outError) *outError = "Missing SN=..."; return false; } + + // Scanner wedge = safest to stay ASCII + if (!isReasonableAscii(*outPartId) || !isReasonableAscii(*outLot) || !isReasonableAscii(*outExp) || !isReasonableAscii(*outSerial)) { + if (outError) *outError = "Only printable ASCII supported for scanner/keyboard mode"; + return false; + } + + // Basic length guard to avoid huge symbols by accident + if (outPartId->size() > 80 || outLot->size() > 80 || outExp->size() > 80 || outSerial->size() > 80) { + if (outError) *outError = "One of the fields is too long (>80 chars)"; + return false; + } + + return true; +} + +QByteArray buildTlvPayload( + const QString& partId, + const QString& lot, + const QString& exp, + const QString& serial, + bool enableCompression, + bool* outCompressed, + QString* outError +) +{ + if (outCompressed) *outCompressed = false; + + if (partId.trimmed().isEmpty()) { if (outError) *outError = "PartId cannot be empty"; return {}; } + if (lot.trimmed().isEmpty()) { if (outError) *outError = "Lot cannot be empty"; return {}; } + if (exp.trimmed().isEmpty()) { if (outError) *outError = "Exp cannot be empty"; return {}; } + if (serial.trimmed().isEmpty()) { if (outError) *outError = "Serial cannot be empty"; return {}; } + + const QByteArray pidB = partId.toUtf8(); + const QByteArray lotB = lot.toUtf8(); + const QByteArray expB = exp.toUtf8(); + const QByteArray snB = serial.toUtf8(); + + if (pidB.size() > 255 || lotB.size() > 255 || expB.size() > 255 || snB.size() > 255) { + if (outError) *outError = "A field exceeds 255 bytes (TLV limit)"; + return {}; + } + + QByteArray body; + tlvAppend(body, kT_PartId, pidB); + tlvAppend(body, kT_Lot, lotB); + tlvAppend(body, kT_ExpDate, expB); + tlvAppend(body, kT_Serial, snB); + + QByteArray payload; + payload.append(char(kPayloadVersion)); + payload.append(char(0x00)); // flags + + if (enableCompression && body.size() >= 60) { + const QByteArray c = qCompress(body, 9); + if (!c.isEmpty() && (c.size() + 2) < (body.size() + 2)) { + payload[1] = char(quint8(payload[1]) | kFlagCompressed); + payload.append(c); + if (outCompressed) *outCompressed = true; + return payload; + } + } + + payload.append(body); + return payload; +} + +bool parseTlvPayload( + const QByteArray& plaintext, + ParsedFields* outFields, + QString* outError +) +{ + qDebug() << plaintext; + if (!outFields) { if (outError) *outError = "Internal error"; return false; } + *outFields = ParsedFields{}; + + if (plaintext.size() < 2) { if (outError) *outError = "Payload too short"; return false; } + + const quint8 pv = quint8(plaintext[0]); + const quint8 pf = quint8(plaintext[1]); + if (pv != kPayloadVersion) { if (outError) *outError = "Unsupported payload version"; return false; } + + QByteArray body = plaintext.mid(2); + if (pf & kFlagCompressed) { + body = qUncompress(body); + if (body.isEmpty()) { if (outError) *outError = "Decompress failed"; return false; } + } + + int i = 0; + while (i + 2 <= body.size()) { + const quint8 t = quint8(body[i++]); + const int len = int(quint8(body[i++])); + if (i + len > body.size()) { if (outError) *outError = "TLV length overflow"; return false; } + const QByteArray v = body.mid(i, len); + i += len; + + switch (t) { + case kT_PartId: outFields->partId = QString::fromUtf8(v); break; + case kT_Lot: outFields->lot = QString::fromUtf8(v); break; + case kT_ExpDate: outFields->exp = QString::fromUtf8(v); break; + case kT_Serial: outFields->serial = QString::fromUtf8(v); break; + default: break; + } + } + + if (outFields->partId.isEmpty()) { if (outError) *outError = "Missing PartId"; return false; } + if (outFields->lot.isEmpty()) { if (outError) *outError = "Missing Lot"; return false; } + if (outFields->exp.isEmpty()) { if (outError) *outError = "Missing Exp"; return false; } + if (outFields->serial.isEmpty()) { if (outError) *outError = "Missing Serial"; return false; } + + return true; +} + +} // namespace diality Index: barcode/common/diality_payload.h =================================================================== diff -u --- barcode/common/diality_payload.h (revision 0) +++ barcode/common/diality_payload.h (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +namespace diality { + +// Payload version and flags +static constexpr quint8 kPayloadVersion = 0x01; +static constexpr quint8 kFlagCompressed = 0x01; + +// TLV types (all values are UTF-8 strings) +static constexpr quint8 kT_PartId = 0x01; // string +static constexpr quint8 kT_Lot = 0x02; // string +static constexpr quint8 kT_ExpDate = 0x03; // string (recommended: YYYYMMDD) +static constexpr quint8 kT_Serial = 0x04; // string + +struct ParsedFields { + QString partId; + QString lot; + QString exp; // keep as string (device can validate/format) + QString serial; +}; + +QByteArray buildTlvPayload( + const QString& partId, + const QString& lot, + const QString& exp, + const QString& serial, + bool enableCompression, + bool* outCompressed, + QString* outError +); + +bool parseTlvPayload( + const QByteArray& plaintext, + ParsedFields* outFields, + QString* outError +); + +// Parse a simple key-value input: +// PID=...|LOT=...|EXP=...|SN=... +// Accepts any string values (trimmed). Case-insensitive keys. +bool parseKeyValueInput( + const QString& input, + QString* outPartId, + QString* outLot, + QString* outExp, + QString* outSerial, + QString* outError +); + +} // namespace diality Index: barcode/common/diality_secure_code.cpp =================================================================== diff -u --- barcode/common/diality_secure_code.cpp (revision 0) +++ barcode/common/diality_secure_code.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,350 @@ +#include "diality_secure_code.h" + +#include +#include +#include + +#include +#include + +namespace diality { + +static constexpr int kAesKeyBytes = 32; +static constexpr int kGcmIvBytes = 12; +static constexpr int kGcmTagBytes = 16; +static constexpr int kEdPkBytes = 32; +static constexpr int kEdSkBytes = 32; +static constexpr int kEdSigBytes = 64; + +static QByteArray b64UrlNoPadEncodeQt(const QByteArray& bin) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + return bin.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); +#else + QByteArray b = bin.toBase64(); + b.replace('+', '-'); + b.replace('/', '_'); + while (!b.isEmpty() && b.endsWith('=')) b.chop(1); + return b; +#endif +} + +static bool b64UrlNoPadDecodeQt(const QByteArray& txt, QByteArray* binOut, QString* errOut) +{ + if (!binOut) { + if (errOut) *errOut = "Internal error: binOut is null"; + return false; + } +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + QByteArray bin = QByteArray::fromBase64(txt, QByteArray::Base64UrlEncoding); + if (bin.isEmpty() && !txt.isEmpty()) { + if (errOut) *errOut = "Base64url decode failed"; + return false; + } + *binOut = bin; + return true; +#else + QByteArray t = txt; + t.replace('-', '+'); + t.replace('_', '/'); + while (t.size() % 4) t.append('='); + QByteArray bin = QByteArray::fromBase64(t); + if (bin.isEmpty() && !txt.isEmpty()) { + if (errOut) *errOut = "Base64url decode failed"; + return false; + } + *binOut = bin; + return true; +#endif +} + +bool cryptoInit() +{ +#if OPENSSL_VERSION_NUMBER < 0x10100000L + OpenSSL_add_all_algorithms(); +#endif + return true; +} + +static bool aes256gcm_encrypt( + const QByteArray& key32, + const unsigned char iv[kGcmIvBytes], + const QByteArray& plaintext, + QByteArray* ciphertextPlusTag, + QString* err +) +{ + if (key32.size() != kAesKeyBytes) { if (err) *err = "Bad AES key size"; return false; } + if (!ciphertextPlusTag) { if (err) *err = "Internal error"; return false; } + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { if (err) *err = "EVP_CIPHER_CTX_new failed"; return false; } + + bool ok = false; + QByteArray ct(plaintext.size(), '\0'); + int outLen = 0; + int total = 0; + unsigned char tag[kGcmTagBytes]; + + do { + if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) { if (err) *err = "EncryptInit (cipher) failed"; break; } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, kGcmIvBytes, nullptr) != 1) { if (err) *err = "Set IV len failed"; break; } + if (EVP_EncryptInit_ex(ctx, nullptr, nullptr, + reinterpret_cast(key32.constData()), + iv) != 1) { if (err) *err = "EncryptInit (key/iv) failed"; break; } + + if (EVP_EncryptUpdate(ctx, + reinterpret_cast(ct.data()), + &outLen, + reinterpret_cast(plaintext.constData()), + plaintext.size()) != 1) { if (err) *err = "EncryptUpdate failed"; break; } + total = outLen; + + if (EVP_EncryptFinal_ex(ctx, + reinterpret_cast(ct.data()) + total, + &outLen) != 1) { if (err) *err = "EncryptFinal failed"; break; } + total += outLen; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, kGcmTagBytes, tag) != 1) { if (err) *err = "Get TAG failed"; break; } + + ct.truncate(total); + QByteArray out; + out.reserve(ct.size() + kGcmTagBytes); + out.append(ct); + out.append(reinterpret_cast(tag), kGcmTagBytes); + + *ciphertextPlusTag = out; + ok = true; + } while (false); + + EVP_CIPHER_CTX_free(ctx); + return ok; +} + +static bool aes256gcm_decrypt( + const QByteArray& key32, + const unsigned char iv[kGcmIvBytes], + const QByteArray& ciphertextPlusTag, + QByteArray* plaintextOut, + QString* err +) +{ + if (key32.size() != kAesKeyBytes) { if (err) *err = "Bad AES key size"; return false; } + if (!plaintextOut) { if (err) *err = "Internal error"; return false; } + if (ciphertextPlusTag.size() < kGcmTagBytes) { if (err) *err = "Ciphertext too short"; return false; } + + const int ctLen = ciphertextPlusTag.size() - kGcmTagBytes; + const unsigned char* tag = reinterpret_cast(ciphertextPlusTag.constData() + ctLen); + + EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); + if (!ctx) { if (err) *err = "EVP_CIPHER_CTX_new failed"; return false; } + + bool ok = false; + QByteArray pt(ctLen, '\0'); + int outLen = 0; + int total = 0; + + do { + if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) { if (err) *err = "DecryptInit (cipher) failed"; break; } + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, kGcmIvBytes, nullptr) != 1) { if (err) *err = "Set IV len failed"; break; } + if (EVP_DecryptInit_ex(ctx, nullptr, nullptr, + reinterpret_cast(key32.constData()), + iv) != 1) { if (err) *err = "DecryptInit (key/iv) failed"; break; } + + if (EVP_DecryptUpdate(ctx, + reinterpret_cast(pt.data()), + &outLen, + reinterpret_cast(ciphertextPlusTag.constData()), + ctLen) != 1) { if (err) *err = "DecryptUpdate failed"; break; } + total = outLen; + + if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, kGcmTagBytes, const_cast(tag)) != 1) { if (err) *err = "Set TAG failed"; break; } + + const int fin = EVP_DecryptFinal_ex(ctx, + reinterpret_cast(pt.data()) + total, + &outLen); + if (fin != 1) { if (err) *err = "Auth check failed (bad tag / tampered)"; break; } + total += outLen; + + pt.truncate(total); + *plaintextOut = pt; + ok = true; + } while (false); + + EVP_CIPHER_CTX_free(ctx); + return ok; +} + +static bool ed25519_sign_detached( + const QByteArray& sk32, + const QByteArray& msg, + QByteArray* sig64, + QString* err +) +{ + if (sk32.size() != kEdSkBytes) { if (err) *err = "Bad Ed25519 private key size"; return false; } + if (!sig64) { if (err) *err = "Internal error"; return false; } + + EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, + reinterpret_cast(sk32.constData()), + kEdSkBytes); + if (!pkey) { if (err) *err = "EVP_PKEY_new_raw_private_key failed"; return false; } + + EVP_MD_CTX* mctx = EVP_MD_CTX_new(); + if (!mctx) { EVP_PKEY_free(pkey); if (err) *err = "EVP_MD_CTX_new failed"; return false; } + + bool ok = false; + size_t sigLen = kEdSigBytes; + QByteArray sig(kEdSigBytes, '\0'); + + do { + if (EVP_DigestSignInit(mctx, nullptr, nullptr, nullptr, pkey) != 1) { if (err) *err = "DigestSignInit failed"; break; } + if (EVP_DigestSign(mctx, + reinterpret_cast(sig.data()), &sigLen, + reinterpret_cast(msg.constData()), + msg.size()) != 1) { if (err) *err = "DigestSign failed"; break; } + if (sigLen != kEdSigBytes) { if (err) *err = "Unexpected signature size"; break; } + *sig64 = sig; + ok = true; + } while (false); + + EVP_MD_CTX_free(mctx); + EVP_PKEY_free(pkey); + return ok; +} + +static bool ed25519_verify_detached( + const QByteArray& pk32, + const QByteArray& msg, + const QByteArray& sig64, + QString* err +) +{ + if (pk32.size() != kEdPkBytes) { if (err) *err = "Bad Ed25519 public key size"; return false; } + if (sig64.size() != kEdSigBytes) { if (err) *err = "Bad signature size"; return false; } + + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, + reinterpret_cast(pk32.constData()), + kEdPkBytes); + if (!pkey) { if (err) *err = "EVP_PKEY_new_raw_public_key failed"; return false; } + + EVP_MD_CTX* mctx = EVP_MD_CTX_new(); + if (!mctx) { EVP_PKEY_free(pkey); if (err) *err = "EVP_MD_CTX_new failed"; return false; } + + bool ok = false; + do { + if (EVP_DigestVerifyInit(mctx, nullptr, nullptr, nullptr, pkey) != 1) { if (err) *err = "DigestVerifyInit failed"; break; } + const int rc = EVP_DigestVerify(mctx, + reinterpret_cast(sig64.constData()), + sig64.size(), + reinterpret_cast(msg.constData()), + msg.size()); + if (rc != 1) { if (err) *err = "Signature invalid"; break; } + ok = true; + } while (false); + + EVP_MD_CTX_free(mctx); + EVP_PKEY_free(pkey); + return ok; +} + +QString buildSecureDatamatrixText( + const QByteArray& plaintext, + const QByteArray& encKey32, + const QByteArray& signSk32, + quint8 keyId +) +{ + if (encKey32.size() != kAesKeyBytes) return QString(); + if (signSk32.size() != kEdSkBytes) return QString(); + + unsigned char iv[kGcmIvBytes]; + if (RAND_bytes(iv, sizeof(iv)) != 1) return QString(); + + QByteArray cipherPlusTag; + QString err; + if (!aes256gcm_encrypt(encKey32, iv, plaintext, &cipherPlusTag, &err)) return QString(); + if (cipherPlusTag.size() > 65535) return QString(); + + QByteArray container; + container.reserve(4 + 1 + 1 + kGcmIvBytes + 2 + cipherPlusTag.size() + kEdSigBytes); + + container.append("DLY1", 4); + container.append(char(0x01)); // Version + container.append(char(keyId)); // KeyId + container.append(reinterpret_cast(iv), kGcmIvBytes); + + const quint16 cLen16 = quint16(cipherPlusTag.size()); + char lenBytes[2]; + qToLittleEndian(cLen16, reinterpret_cast(lenBytes)); + container.append(lenBytes, 2); + container.append(cipherPlusTag); + + QByteArray sig; + if (!ed25519_sign_detached(signSk32, container, &sig, &err)) return QString(); + + container.append(sig); + + const QByteArray b64 = b64UrlNoPadEncodeQt(container); + return QString("DLY1.%1").arg(QString::fromLatin1(b64)); +} + +SecureCodeResult openSecureDatamatrixText( + const QString& scannedText, + const QByteArray& encKey32, + const QByteArray& signPk32 +) +{ + SecureCodeResult r; + + if (encKey32.size() != kAesKeyBytes) { r.error = "Bad encryption key size"; return r; } + if (signPk32.size() != kEdPkBytes) { r.error = "Bad signing public key size"; return r; } + + const QString prefix = "DLY1."; + if (!scannedText.startsWith(prefix)) { r.error = "Invalid prefix"; return r; } + + QByteArray bin; + QString derr; + if (!b64UrlNoPadDecodeQt(scannedText.mid(prefix.size()).toLatin1(), &bin, &derr)) { r.error = derr; return r; } + + const int minLen = 4 + 1 + 1 + kGcmIvBytes + 2 + kEdSigBytes; + if (bin.size() < minLen) { r.error = "Container too short"; return r; } + + const int sigOff = bin.size() - kEdSigBytes; + const QByteArray signedPart = bin.left(sigOff); + const QByteArray sigPart = bin.mid(sigOff, kEdSigBytes); + + QString err; + if (!ed25519_verify_detached(signPk32, signedPart, sigPart, &err)) { + r.error = "Signature invalid (not Diality-generated)"; + return r; + } + + const char* p = signedPart.constData(); + if (std::memcmp(p, "DLY1", 4) != 0) { r.error = "Bad magic"; return r; } + + const quint8 version = quint8(p[4]); + if (version != 0x01) { r.error = "Unsupported version"; return r; } + + const int headerLen = 4 + 1 + 1 + kGcmIvBytes + 2; + const unsigned char* iv = reinterpret_cast(p + 6); + const uchar* lenPtr = reinterpret_cast(p + 6 + kGcmIvBytes); + const quint16 cLen = qFromLittleEndian(lenPtr); + + if (headerLen + int(cLen) != signedPart.size()) { r.error = "Length mismatch"; return r; } + + const QByteArray cipherPlusTag = signedPart.mid(headerLen, int(cLen)); + QByteArray plain; + qDebug() << " ~ c ~ " << cipherPlusTag; + qDebug() << " ~ p ~ " << plain; + if (!aes256gcm_decrypt(encKey32, iv, cipherPlusTag, &plain, &err)) { + r.error = "Decryption failed (tampered or wrong key)"; + return r; + } + + r.ok = true; + r.plaintext = plain; + return r; +} + +} // namespace diality Index: barcode/common/diality_secure_code.h =================================================================== diff -u --- barcode/common/diality_secure_code.h (revision 0) +++ barcode/common/diality_secure_code.h (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace diality { + +struct SecureCodeResult { + bool ok = false; + QString error; + QByteArray plaintext; +}; + +bool cryptoInit(); + +QString buildSecureDatamatrixText( + const QByteArray& plaintext, + const QByteArray& encKey32, + const QByteArray& signSk32, + quint8 keyId = 0x01 +); + +SecureCodeResult openSecureDatamatrixText( + const QString& scannedText, + const QByteArray& encKey32, + const QByteArray& signPk32 +); + +} // namespace diality Index: barcode/device_reader/device_reader.pro =================================================================== diff -u --- barcode/device_reader/device_reader.pro (revision 0) +++ barcode/device_reader/device_reader.pro (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,24 @@ +QT += widgets +CONFIG += c++17 + +TEMPLATE = app +TARGET = device_reader + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + ../common/diality_secure_code.cpp \ + ../common/diality_keys.cpp \ + ../common/diality_payload.cpp + +HEADERS += \ + mainwindow.h \ + ../common/diality_secure_code.h \ + ../common/diality_keys.h \ + ../common/diality_payload.h + +INCLUDEPATH += \ + ../common + +INCLUDEPATH += $$OPENSSL_INCLUDE_DIR +LIBS += -L$$OPENSSL_LIB_DIR -lssl -lcrypto Index: barcode/device_reader/device_reader.pro.user =================================================================== diff -u --- barcode/device_reader/device_reader.pro.user (revision 0) +++ barcode/device_reader/device_reader.pro.user (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,272 @@ + + + + + + EnvironmentId + {1bb8c257-238c-45ae-95a5-640af38006c9} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {dd9780cb-0f43-4109-afa0-38bcc47b5fcd} + 0 + 0 + 0 + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Debug + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Debug + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + + + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Release + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Release + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Profile + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Profile + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + Qt4ProjectManager.Qt4RunConfiguration: + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/device_reader.pro + false + true + true + true + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/device_reader/build/Desktop-Debug + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + Index: barcode/device_reader/main.cpp =================================================================== diff -u --- barcode/device_reader/main.cpp (revision 0) +++ barcode/device_reader/main.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,10 @@ +#include +#include "mainwindow.h" + +int main(int argc, char** argv) +{ + QApplication app(argc, argv); + MainWindow w; + w.show(); + return app.exec(); +} Index: barcode/device_reader/mainwindow.cpp =================================================================== diff -u --- barcode/device_reader/mainwindow.cpp (revision 0) +++ barcode/device_reader/mainwindow.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,104 @@ +#include "mainwindow.h" +#include "../common/diality_secure_code.h" +#include "../common/diality_keys.h" +#include "../common/diality_payload.h" + +#include +#include +#include +#include +#include +#include + +static QString formatFields(const diality::ParsedFields& f) +{ + QString out; + out += "ID:" + f.partId + "\n"; + out += "Lt:" + f.lot + "\n"; + out += "Ex:" + f.exp + "\n"; + out += "SN:" + f.serial + "\n"; + return out; +} + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) +{ + diality::cryptoInit(); + + auto* central = new QWidget(this); + auto* root = new QVBoxLayout(central); + + auto* keysRow = new QHBoxLayout(); + m_keysStatus = new QLabel("Keys: not loaded", central); + m_reloadKeysBtn = new QPushButton("Reload keys", central); + keysRow->addWidget(m_keysStatus, 1); + keysRow->addWidget(m_reloadKeysBtn, 0); + + m_scanEdit = new QLineEdit(central); + m_scanEdit->setPlaceholderText("Scan Data Matrix here (focus stays here)"); + + m_output = new QTextEdit(central); + m_output->setReadOnly(true); + + root->addLayout(keysRow); + root->addWidget(m_scanEdit); + root->addWidget(m_output); + setCentralWidget(central); + + connect(m_reloadKeysBtn , &QPushButton ::clicked , this, &MainWindow::onReloadKeysClicked); + connect(m_scanEdit , &QLineEdit ::returnPressed , this, &MainWindow::onScanEntered ); + + onReloadKeysClicked(); + m_scanEdit->setFocus(); +} + +void MainWindow::onReloadKeysClicked() +{ + QString err; + QString path; + diality::Keys k; + + if (!diality::loadDeviceKeys(&k, &path, &err)) { + m_keysLoaded = false; + m_keys = diality::Keys{}; + m_keysPath.clear(); + m_keysStatus->setText("Keys: NOT LOADED (" + err + "). Expected /etc/diality/keys.json"); + return; + } + + m_keysLoaded = true; + m_keys = k; + m_keysPath = path; + m_keysStatus->setText("Keys: loaded from " + m_keysPath); +} + +bool MainWindow::ensureKeysLoaded() +{ + if (m_keysLoaded) return true; + m_output->setPlainText("ERROR: keys not loaded. Place /etc/diality/keys.json and press Reload."); + return false; +} + +void MainWindow::onScanEntered() +{ + if (!ensureKeysLoaded()) return; + + const QString scanned = m_scanEdit->text().trimmed(); + m_scanEdit->clear(); + if (scanned.isEmpty()) return; + + const auto res = diality::openSecureDatamatrixText(scanned, m_keys.encKey32, m_keys.signPk32); + if (!res.ok) { + m_output->setPlainText("ERROR: " + res.error); + return; + } + + diality::ParsedFields fields; + QString perr; + if (!diality::parseTlvPayload(res.plaintext, &fields, &perr)) { + m_output->setPlainText("ERROR: Payload parse failed: " + perr); + return; + } + + m_output->setPlainText(formatFields(fields)); +} Index: barcode/device_reader/mainwindow.h =================================================================== diff -u --- barcode/device_reader/mainwindow.h (revision 0) +++ barcode/device_reader/mainwindow.h (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,33 @@ +#pragma once + +#include +#include "../common/diality_keys.h" + +class QLineEdit; +class QTextEdit; +class QLabel; +class QPushButton; + +class MainWindow final : public QMainWindow +{ + Q_OBJECT +public: + explicit MainWindow(QWidget* parent = nullptr); + +private slots: + void onReloadKeysClicked(); + void onScanEntered(); + +private: + bool ensureKeysLoaded(); + + QLineEdit* m_scanEdit = nullptr; + QTextEdit* m_output = nullptr; + + QLabel* m_keysStatus = nullptr; + QPushButton* m_reloadKeysBtn = nullptr; + + diality::Keys m_keys; + bool m_keysLoaded = false; + QString m_keysPath; +}; Index: barcode/diality_keygen/diality_keygen.pro =================================================================== diff -u --- barcode/diality_keygen/diality_keygen.pro (revision 0) +++ barcode/diality_keygen/diality_keygen.pro (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,12 @@ +QT -= gui +QT += core +CONFIG += c++17 console +CONFIG -= app_bundle + +TEMPLATE = app +TARGET = diality_keygen + +SOURCES += main.cpp + +INCLUDEPATH += $$OPENSSL_INCLUDE_DIR +LIBS += -L$$OPENSSL_LIB_DIR -lssl -lcrypto Index: barcode/diality_keygen/diality_keygen.pro.user =================================================================== diff -u --- barcode/diality_keygen/diality_keygen.pro.user (revision 0) +++ barcode/diality_keygen/diality_keygen.pro.user (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,270 @@ + + + + + + EnvironmentId + {1bb8c257-238c-45ae-95a5-640af38006c9} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {dd9780cb-0f43-4109-afa0-38bcc47b5fcd} + 0 + 0 + 0 + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Debug + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Debug + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + + + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Release + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Release + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Profile + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/diality_keygen/build/Desktop-Profile + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + ProjectExplorer.CustomExecutableRunConfiguration + + false + true + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + Index: barcode/diality_keygen/main.cpp =================================================================== diff -u --- barcode/diality_keygen/main.cpp (revision 0) +++ barcode/diality_keygen/main.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +static QByteArray toHex(const unsigned char* data, int len) +{ + return QByteArray(reinterpret_cast(data), len).toHex(); +} + +int main(int argc, char* argv[]) +{ + QCoreApplication app(argc, argv); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + OpenSSL_add_all_algorithms(); +#endif + + QTextStream out(stdout); + QTextStream err(stderr); + + unsigned char encKey[32]; + if (RAND_bytes(encKey, sizeof(encKey)) != 1) { + err << "Failed to generate encryption key\n"; + return 1; + } + + EVP_PKEY_CTX* kctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + if (!kctx) { + err << "EVP_PKEY_CTX_new_id failed\n"; + return 1; + } + if (EVP_PKEY_keygen_init(kctx) != 1) { + err << "EVP_PKEY_keygen_init failed\n"; + EVP_PKEY_CTX_free(kctx); + return 1; + } + + EVP_PKEY* pkey = nullptr; + if (EVP_PKEY_keygen(kctx, &pkey) != 1) { + err << "EVP_PKEY_keygen failed\n"; + EVP_PKEY_CTX_free(kctx); + return 1; + } + EVP_PKEY_CTX_free(kctx); + + unsigned char sk[32]; + unsigned char pk[32]; + size_t skLen = sizeof(sk); + size_t pkLen = sizeof(pk); + + if (EVP_PKEY_get_raw_private_key(pkey, sk, &skLen) != 1 || skLen != 32) { + err << "Failed to extract Ed25519 private key\n"; + EVP_PKEY_free(pkey); + return 1; + } + if (EVP_PKEY_get_raw_public_key(pkey, pk, &pkLen) != 1 || pkLen != 32) { + err << "Failed to extract Ed25519 public key\n"; + EVP_PKEY_free(pkey); + return 1; + } + EVP_PKEY_free(pkey); + + QJsonObject root; + root["encKey32"] = QString::fromLatin1(toHex(encKey, 32)); + root["signSk32"] = QString::fromLatin1(toHex(sk, 32)); + root["signPk32"] = QString::fromLatin1(toHex(pk, 32)); + + QJsonDocument doc(root); + + QString outPath = "keys.json"; + if (app.arguments().size() >= 2) { + outPath = app.arguments().at(1); + } + + QFile f(outPath); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + err << "Failed to write " << outPath << "\n"; + return 1; + } + f.write(doc.toJson(QJsonDocument::Indented)); + f.close(); + + out << "Keys generated successfully:\n\n"; + out << "encKey32 : " << root["encKey32"].toString() << "\n"; + out << "signSk32 : " << root["signSk32"].toString() << " (PC ONLY)\n"; + out << "signPk32 : " << root["signPk32"].toString() << " (DEVICE)\n\n"; + out << "Written to " << outPath << "\n"; + return 0; +} Index: barcode/example.txt =================================================================== diff -u --- barcode/example.txt (revision 0) +++ barcode/example.txt (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,2 @@ +PID=0107350000000023|SN=211C90KW6R7YH8S1Q1F81T|EXP=17200601|LOT=10ABCD12345 +PID=07350000000023|SN=1C90KW6R7YH8S1Q1F81T|EXP=200601|LOT=ABCD12345 Index: barcode/pc_label_tool/main.cpp =================================================================== diff -u --- barcode/pc_label_tool/main.cpp (revision 0) +++ barcode/pc_label_tool/main.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,10 @@ +#include +#include "mainwindow.h" + +int main(int argc, char** argv) +{ + QApplication app(argc, argv); + MainWindow w; + w.show(); + return app.exec(); +} Index: barcode/pc_label_tool/mainwindow.cpp =================================================================== diff -u --- barcode/pc_label_tool/mainwindow.cpp (revision 0) +++ barcode/pc_label_tool/mainwindow.cpp (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,258 @@ +#include "mainwindow.h" +#include "../common/diality_secure_code.h" +#include "../common/diality_keys.h" +#include "../common/diality_payload.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static QImage bitMatrixToImage(const ZXing::BitMatrix& bm, int scale, int quietModules) +{ + const int w = bm.width(); + const int h = bm.height(); + const int imgW = (w + 2 * quietModules) * scale; + const int imgH = (h + 2 * quietModules) * scale; + + QImage img(imgW, imgH, QImage::Format_Grayscale8); + img.fill(255); + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + if (!bm.get(x, y)) continue; + + const int px = (x + quietModules) * scale; + const int py = (y + quietModules) * scale; + + for (int dy = 0; dy < scale; ++dy) { + uchar* row = img.scanLine(py + dy); + for (int dx = 0; dx < scale; ++dx) { + row[px + dx] = 0; + } + } + } + } + return img; +} + +static int computeScaleToFitMm(int modules, int quietModules, int printerDpi, double targetMm) +{ + if (modules <= 0 || printerDpi <= 0 || targetMm <= 0.0) return 1; + + const double targetInches = targetMm / 25.4; + const double targetPixels = targetInches * double(printerDpi); + const int totalModules = modules + 2 * quietModules; + + const int scale = int(targetPixels / double(totalModules)); + return (scale < 1) ? 1 : scale; +} + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) +{ + diality::cryptoInit(); + + auto* central = new QWidget(this); + auto* root = new QVBoxLayout(central); + + auto* keysRow = new QHBoxLayout(); + m_keysStatus = new QLabel("Keys: not loaded", central); + m_reloadKeysBtn = new QPushButton("Reload keys.json", central); + keysRow->addWidget(m_keysStatus, 1); + keysRow->addWidget(m_reloadKeysBtn, 0); + + m_fieldsEdit = new QLineEdit(central); + m_fieldsEdit->setPlaceholderText("Enter: PID=CARTRIDGE-123|LOT=A24X7|EXP=20271231|SN=0000009876543210"); + + auto* settingsRow = new QHBoxLayout(); + m_dpiEdit = new QLineEdit(central); + m_dpiEdit->setPlaceholderText("Printer DPI (e.g., 300)"); + m_dpiEdit->setText("300"); + + m_targetMmEdit = new QLineEdit(central); + m_targetMmEdit->setPlaceholderText("Max size mm (e.g., 14.4)"); + m_targetMmEdit->setText("14.4"); + + m_compressCheck = new QCheckBox("Compress if beneficial", central); + m_compressCheck->setChecked(true); + + settingsRow->addWidget(new QLabel("DPI:", central)); + settingsRow->addWidget(m_dpiEdit); + settingsRow->addSpacing(12); + settingsRow->addWidget(new QLabel("Max mm:", central)); + settingsRow->addWidget(m_targetMmEdit); + settingsRow->addSpacing(12); + settingsRow->addWidget(m_compressCheck, 1); + + m_sizeStatus = new QLabel("Size: (not generated)", central); + + auto* btnRow = new QHBoxLayout(); + auto* genBtn = new QPushButton("Generate + Render", central); + m_saveBtn = new QPushButton("Save PNG...", central); + m_saveBtn->setEnabled(false); + btnRow->addWidget(genBtn); + btnRow->addWidget(m_saveBtn); + + m_outText = new QTextEdit(central); + m_outText->setReadOnly(true); + m_outText->setPlaceholderText("Scanner text will appear here"); + + m_imageLabel = new QLabel(central); + m_imageLabel->setMinimumSize(240, 240); + m_imageLabel->setScaledContents(false); + + root->addLayout(keysRow); + root->addWidget(m_fieldsEdit); + root->addLayout(settingsRow); + root->addWidget(m_sizeStatus); + root->addLayout(btnRow); + root->addWidget(m_outText); + root->addWidget(m_imageLabel); + + setCentralWidget(central); + + connect(m_reloadKeysBtn, &QPushButton::clicked, this, &MainWindow::onReloadKeysClicked); + connect(genBtn, &QPushButton::clicked, this, &MainWindow::onGenerateClicked); + connect(m_saveBtn, &QPushButton::clicked, this, &MainWindow::onSaveClicked); + + onReloadKeysClicked(); +} + +void MainWindow::onReloadKeysClicked() +{ + QString err; + QString path; + diality::Keys k; + + if (!diality::loadPcKeys(&k, &path, &err)) { + m_keysLoaded = false; + m_keys = diality::Keys{}; + m_keysPath.clear(); + m_keysStatus->setText("Keys: NOT LOADED (" + err + "). Place keys.json next to the executable."); + return; + } + + m_keysLoaded = true; + m_keys = k; + m_keysPath = path; + m_keysStatus->setText("Keys: loaded from " + m_keysPath); +} + +bool MainWindow::ensureKeysLoaded() +{ + if (m_keysLoaded) return true; + QMessageBox::warning(this, "Keys", "keys.json is not loaded. Place keys.json next to the executable and click Reload."); + return false; +} + +void MainWindow::updateSizeStatus(int modules, int scalePx, int quietModules, double printedMm) +{ + m_sizeStatus->setText(QString("Size: %1x%2 modules, quiet=%3, scale=%4 px/module, printed=%5 mm (target %6 mm)") + .arg(modules).arg(modules).arg(quietModules).arg(scalePx) + .arg(QString::number(printedMm, 'f', 2)) + .arg(m_targetMmEdit->text().trimmed())); +} + +void MainWindow::onGenerateClicked() +{ + if (!ensureKeysLoaded()) return; + + QString partId; + QString lot; + QString exp; + QString sn; + + QString perr; + if (!diality::parseKeyValueInput(m_fieldsEdit->text(), &partId, &lot, &exp, &sn, &perr)) { + QMessageBox::warning(this, "Input", perr); + return; + } + + bool compressed = false; + QString derr; + const bool enableCompression = m_compressCheck->isChecked(); + const QByteArray payload = diality::buildTlvPayload(partId, lot, exp, sn, enableCompression, &compressed, &derr); + if (payload.isEmpty()) { + QMessageBox::critical(this, "Payload", derr.isEmpty() ? "Failed to build payload" : derr); + return; + } + + const QString secureText = diality::buildSecureDatamatrixText(payload, m_keys.encKey32, m_keys.signSk32, 0x01); + if (secureText.isEmpty()) { + QMessageBox::critical(this, "Generate", "Failed to build secure text (crypto error)"); + return; + } + + m_outText->setPlainText(secureText + (compressed ? "\n(compressed payload)" : "\n(uncompressed payload)")); + + ZXing::MultiFormatWriter writer(ZXing::BarcodeFormat::DataMatrix); + writer.setMargin(0); + + const ZXing::BitMatrix bm = writer.encode(secureText.toStdWString(), 0, 0); + + const int quiet = 2; + + bool okDpi = false; + const int dpi = m_dpiEdit->text().trimmed().toInt(&okDpi); + bool okMm = false; + const double targetMm = m_targetMmEdit->text().trimmed().toDouble(&okMm); + + const int modules = bm.width(); + const int useDpi = okDpi ? dpi : 300; + const double useMm = okMm ? targetMm : 14.4; + + const int scale = computeScaleToFitMm(modules, quiet, useDpi, useMm); + + const int totalModules = modules + 2 * quiet; + const double printedInches = double(totalModules * scale) / double(useDpi); + const double printedMm = printedInches * 25.4; + + updateSizeStatus(modules, scale, quiet, printedMm); + + if (printedMm > useMm + 0.01) { + QMessageBox::warning(this, "Size", + "Auto-fit could not meet the target size at this DPI. " + "Try higher DPI or reduce payload."); + } + if (scale < 2) { + QMessageBox::critical(this, "Readability", + "Symbol is too dense to be reliably scanned at this size/DPI (scale < 2 px/module).\n\n" + "Fix: increase DPI (e.g., 600), increase Max mm, or shorten fields."); + return; + } + + if (scale < 3) { + QMessageBox::warning(this, "Readability", + "Computed module size is very small (scale < 2 px/module). " + "Scanner reliability may suffer. Consider higher DPI or smaller payload."); + } + + m_lastImage = bitMatrixToImage(bm, scale, quiet); + m_imageLabel->setPixmap(QPixmap::fromImage(m_lastImage)); + m_saveBtn->setEnabled(true); +} + +void MainWindow::onSaveClicked() +{ + if (m_lastImage.isNull()) return; + + const QString path = QFileDialog::getSaveFileName(this, "Save Data Matrix", "diality_dm.png", "PNG (*.png)"); + if (path.isEmpty()) return; + + QImageWriter w(path, "png"); + if (!w.write(m_lastImage)) { + QMessageBox::critical(this, "Save", w.errorString()); + } +} Index: barcode/pc_label_tool/mainwindow.h =================================================================== diff -u --- barcode/pc_label_tool/mainwindow.h (revision 0) +++ barcode/pc_label_tool/mainwindow.h (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include "../common/diality_keys.h" + +class QLineEdit; +class QTextEdit; +class QLabel; +class QPushButton; +class QCheckBox; + +class MainWindow final : public QMainWindow +{ + Q_OBJECT +public: + explicit MainWindow(QWidget* parent = nullptr); + +private slots: + void onReloadKeysClicked(); + void onGenerateClicked(); + void onSaveClicked(); + +private: + bool ensureKeysLoaded(); + void updateSizeStatus(int modules, int scalePx, int quietModules, double printedMm); + + QLabel* m_keysStatus = nullptr; + QPushButton* m_reloadKeysBtn = nullptr; + + QLineEdit* m_fieldsEdit = nullptr; + QLineEdit* m_dpiEdit = nullptr; + QLineEdit* m_targetMmEdit = nullptr; + QCheckBox* m_compressCheck = nullptr; + + QLabel* m_sizeStatus = nullptr; + + QTextEdit* m_outText = nullptr; + QLabel* m_imageLabel = nullptr; + QPushButton* m_saveBtn = nullptr; + + QImage m_lastImage; + + diality::Keys m_keys; + bool m_keysLoaded = false; + QString m_keysPath; +}; Index: barcode/pc_label_tool/pc_label_tool.pro =================================================================== diff -u --- barcode/pc_label_tool/pc_label_tool.pro (revision 0) +++ barcode/pc_label_tool/pc_label_tool.pro (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,27 @@ +QT += widgets +CONFIG += c++17 + +TEMPLATE = app +TARGET = pc_label_tool + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + ../common/diality_secure_code.cpp \ + ../common/diality_keys.cpp \ + ../common/diality_payload.cpp + +HEADERS += \ + mainwindow.h \ + ../common/diality_secure_code.h \ + ../common/diality_keys.h \ + ../common/diality_payload.h + +INCLUDEPATH += \ + ../common + +INCLUDEPATH += $$OPENSSL_INCLUDE_DIR +LIBS += -L$$OPENSSL_LIB_DIR -lssl -lcrypto + +INCLUDEPATH += $$ZXING_INCLUDE_DIR +LIBS += -L$$ZXING_LIB_DIR -lZXing Index: barcode/pc_label_tool/pc_label_tool.pro.user =================================================================== diff -u --- barcode/pc_label_tool/pc_label_tool.pro.user (revision 0) +++ barcode/pc_label_tool/pc_label_tool.pro.user (revision f33526efb86190f37d0eaadbe5cad09830c59266) @@ -0,0 +1,272 @@ + + + + + + EnvironmentId + {1bb8c257-238c-45ae-95a5-640af38006c9} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + true + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 0 + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 4 + true + + + + true + + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {dd9780cb-0f43-4109-afa0-38bcc47b5fcd} + 0 + 0 + 0 + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Debug + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Debug + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + + + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Release + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Release + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + + + 0 + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Profile + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Profile + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + clean + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 0 + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + + Qt4ProjectManager.Qt4RunConfiguration: + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/pc_label_tool.pro + false + true + true + true + /home/denali/Desktop/Projects/tests/diality_codes_qmake_openssl_keysjson_tlv_autofit_strings/pc_label_tool/build/Desktop-Debug + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + +