// Copyright (C) 2021 The Qt Company Ltd. // Copyright (C) 2019 Luxoft Sweden AB // Copyright (C) 2018 Pelagicore AG // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // Qt-Security score:critical reason:data-parser #include #include #include #include #include #include #include "global.h" #include "qtyaml.h" #include "packageinfo.h" #include "utilities.h" #include "exception.h" #include "installationreport.h" using namespace Qt::StringLiterals; QT_BEGIN_NAMESPACE_AM // you can generate a new set with // xxd -i <(dd if=/dev/urandom bs=64 count=1) static const unsigned char privateHmacKeyData[64] = { 0xd8, 0xde, 0x41, 0x25, 0xee, 0x24, 0xd0, 0x19, 0xa2, 0x43, 0x06, 0x22, 0x30, 0xa4, 0x87, 0xf0, 0x12, 0x07, 0xe9, 0xd3, 0x1c, 0xd4, 0x6f, 0xd6, 0x1c, 0xc5, 0x38, 0x22, 0x2d, 0x7a, 0xe9, 0x90, 0x1e, 0xdf, 0xc8, 0x85, 0x86, 0x96, 0xc4, 0x64, 0xc5, 0x59, 0xee, 0xc4, 0x69, 0xb6, 0x0f, 0x94, 0x5c, 0xb0, 0x2a, 0xf0, 0xf1, 0xc0, 0x8a, 0x7a, 0xf0, 0xf6, 0x3f, 0x17, 0xe6, 0xab, 0x2e, 0xc7 }; InstallationReport::InstallationReport(const QString &packageId) : m_packageId(packageId) { } QString InstallationReport::packageId() const { return m_packageId; } void InstallationReport::setPackageId(const QString &packageId) { m_packageId = packageId; } QVariantMap InstallationReport::extraMetaData() const { return m_extraMetaData; } void InstallationReport::setExtraMetaData(const QVariantMap &extraMetaData) { m_extraMetaData = extraMetaData; } QVariantMap InstallationReport::extraSignedMetaData() const { return m_extraSignedMetaData; } void InstallationReport::setExtraSignedMetaData(const QVariantMap &extraSignedMetaData) { m_extraSignedMetaData = extraSignedMetaData; } QByteArray InstallationReport::digest() const { return m_digest; } void InstallationReport::setDigest(const QByteArray &digest) { m_digest = digest; } quint64 InstallationReport::diskSpaceUsed() const { return m_diskSpaceUsed; } void InstallationReport::setDiskSpaceUsed(quint64 diskSpaceUsed) { m_diskSpaceUsed = diskSpaceUsed; } QByteArray InstallationReport::developerSignature() const { return m_developerSignature; } void InstallationReport::setDeveloperSignature(const QByteArray &developerSignature) { m_developerSignature = developerSignature; } QByteArray InstallationReport::storeSignature() const { return m_storeSignature; } void InstallationReport::setStoreSignature(const QByteArray &storeSignature) { m_storeSignature = storeSignature; } QStringList InstallationReport::files() const { return m_files; } void InstallationReport::addFile(const QString &file) { m_files << file.normalized(QString::NormalizationForm_C); } void InstallationReport::addFiles(const QStringList &files) { m_files << files; } bool InstallationReport::isValid() const { return PackageInfo::isValidApplicationId(m_packageId) && !m_digest.isEmpty() && !m_files.isEmpty(); } void InstallationReport::deserialize(QIODevice *from) { if (!from || !from->isReadable() || (from->size() > 2*1024*1024)) throw Exception("Installation report is invalid"); m_digest.clear(); m_files.clear(); auto docs = YamlParser::parseAllDocuments(from->readAll()); checkYamlFormat(docs, 3 /*number of expected docs*/, { { u"am-installation-report"_s, 3 } }); const QVariantMap &root = docs.at(1).toMap(); try { if (m_packageId.isEmpty()) { m_packageId = root[u"packageId"_s].toString(); if (m_packageId.isEmpty()) throw Exception("packageId is empty"); } else if (root[u"packageId"_s].toString() != m_packageId) { throw Exception("packageId does not match: expected '%1', but got '%2'") .arg(m_packageId).arg(root[u"packageId"_s].toString()); } m_diskSpaceUsed = root[u"diskSpaceUsed"_s].toULongLong(); m_digest = QByteArray::fromHex(root[u"digest"_s].toString().toLatin1()); if (m_digest.isEmpty()) throw Exception("digest is empty"); auto devSig = root.find(u"developerSignature"_s); if (devSig != root.end()) { m_developerSignature = QByteArray::fromBase64(devSig.value().toString().toLatin1()); if (m_developerSignature.isEmpty()) throw Exception("developerSignature is empty"); } auto storeSig = root.find(u"storeSignature"_s); if (storeSig != root.end()) { m_storeSignature = QByteArray::fromBase64(storeSig.value().toString().toLatin1()); if (m_storeSignature.isEmpty()) throw Exception("storeSignature is empty"); } auto extra = root.find(u"extra"_s); if (extra != root.end()) { m_extraMetaData = extra.value().toMap(); if (m_extraMetaData.isEmpty()) throw Exception("extra metadata is empty"); } auto extraSigned = root.find(u"extraSigned"_s); if (extraSigned != root.end()) { m_extraSignedMetaData = extraSigned.value().toMap(); if (m_extraSignedMetaData.isEmpty()) throw Exception("extraSigned metadata is empty"); } m_files = root[u"files"_s].toStringList(); if (m_files.isEmpty()) throw Exception("No files"); // see if the file has been tampered with by checking the hmac QByteArray hmacFile = QByteArray::fromHex(docs[2].toMap().value(u"hmac"_s).toString().toLatin1()); QByteArray hmacKey = QByteArray::fromRawData(reinterpret_cast(privateHmacKeyData), sizeof(privateHmacKeyData)); QByteArray out = QtYaml::yamlFromVariantDocuments({ docs[0], docs[1] }, QtYaml::BlockStyle); QByteArray hmacCalc= QMessageAuthenticationCode::hash(out, hmacKey, QCryptographicHash::Sha256); if (hmacFile != hmacCalc) { throw Exception("HMAC does not match: expected '%1', but got '%2'") .arg(hmacCalc.toHex()).arg(hmacFile.toHex()); } } catch (const Exception &) { m_digest.clear(); m_diskSpaceUsed = 0; m_files.clear(); throw; } } bool InstallationReport::serialize(QIODevice *to) const { if (!isValid() || !to || !to->isWritable()) return false; QVariantMap header { { u"formatVersion"_s, 3 }, { u"formatType"_s, u"am-installation-report"_s } }; QVariantMap root { { u"packageId"_s, packageId() }, { u"diskSpaceUsed"_s, diskSpaceUsed() }, { u"digest"_s, QString::fromLatin1(digest().toHex()) } }; if (!m_developerSignature.isEmpty()) root[u"developerSignature"_s] = QString::fromLatin1(m_developerSignature.toBase64()); if (!m_storeSignature.isEmpty()) root[u"storeSignature"_s] = QString::fromLatin1(m_storeSignature.toBase64()); if (!m_extraMetaData.isEmpty()) root[u"extra"_s] = m_extraMetaData; if (!m_extraSignedMetaData.isEmpty()) root[u"extraSigned"_s] = m_extraSignedMetaData; root[u"files"_s] = files(); QVector docs; docs << header; docs << root; // generate hmac to prevent tampering QByteArray hmacKey = QByteArray::fromRawData(reinterpret_cast(privateHmacKeyData), sizeof(privateHmacKeyData)); QByteArray out = QtYaml::yamlFromVariantDocuments({ docs[0], docs[1] }, QtYaml::BlockStyle); QByteArray hmacCalc= QMessageAuthenticationCode::hash(out, hmacKey, QCryptographicHash::Sha256); // add another YAML document with a single key/value (way faster than using QtYaml) out += "---\nhmac: '"; out += hmacCalc.toHex(); out += "'\n"; return (to->write(out) == out.size()); } QT_END_NAMESPACE_AM