summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArttu Tarkiainen <arttu.tarkiainen@qt.io>2023-03-30 17:47:46 +0300
committerArttu Tarkiainen <arttu.tarkiainen@qt.io>2023-04-03 15:53:44 +0300
commit6c405418d25475aecc9d1a18bad6da731df6204c (patch)
treeb600996226137d3bd5fdd8b4acb92f5da506e2f8 /src
parent8ed8ee2777d729c1db43bf46023aa9c947363d15 (diff)
Verify meta file integrity from cache
Calculate and store a checksum for each included file when extracing meta.7z archives. When the respective meta item will be loaded from cache on the next run, each file referenced in its Updates.xml will be checked from disk, that it exist and that its checksum matches the one calculated when the file was originally extracted. If the files in cache are not intact, the meta item will be refreshed (downloaded and extracted again). Also bump the IFW cache format version, as the existing caches need to be repopulated to include the new checksum files. Task-number: QTIFW-3023 Change-Id: I6a8431ffb52cfff69e3cb84cccd4b3d5a1fe21da Reviewed-by: Katja Marttila <katja.marttila@qt.io>
Diffstat (limited to 'src')
-rw-r--r--src/libs/installer/metadata.cpp145
-rw-r--r--src/libs/installer/metadata.h5
-rw-r--r--src/libs/installer/metadatajob.cpp3
-rw-r--r--src/libs/installer/metadatajob_p.h39
4 files changed, 184 insertions, 8 deletions
diff --git a/src/libs/installer/metadata.cpp b/src/libs/installer/metadata.cpp
index 9ae817127..e0ff7f614 100644
--- a/src/libs/installer/metadata.cpp
+++ b/src/libs/installer/metadata.cpp
@@ -46,6 +46,60 @@ namespace QInstaller {
\brief The Metadata class represents fetched metadata from a repository.
*/
+
+/*!
+ \internal
+*/
+static bool verifyFileIntegrityFromElement(const QDomElement &element, const QString &childNodeName,
+ const QString &attribute, const QString &metaDirectory, bool testChecksum)
+{
+ const QDomNodeList nodes = element.childNodes();
+ for (int i = 0; i < nodes.count(); ++i) {
+ const QDomNode node = nodes.at(i);
+ if (node.nodeName() != childNodeName)
+ continue;
+
+ const QDir dir(metaDirectory);
+ const QString filename = attribute.isEmpty()
+ ? node.toElement().text()
+ : node.toElement().attribute(attribute);
+
+ if (filename.isEmpty())
+ continue;
+
+ QFile file(dir.absolutePath() + QDir::separator() + filename);
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCWarning(QInstaller::lcInstallerInstallLog)
+ << "Cannot open" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+
+ if (!testChecksum)
+ continue;
+
+ QFile hashFile(file.fileName() + QLatin1String(".sha1"));
+ if (!hashFile.open(QIODevice::ReadOnly)) {
+ qCWarning(QInstaller::lcInstallerInstallLog)
+ << "Cannot open" << hashFile.fileName()
+ << "for reading:" << hashFile.errorString();
+ return false;
+ }
+
+ QCryptographicHash hash(QCryptographicHash::Sha1);
+ hash.addData(&file);
+
+ const QByteArray checksum = hash.result().toHex();
+ const QByteArray expectedChecksum = hashFile.readAll();
+ if (checksum != expectedChecksum) {
+ qCWarning(QInstaller::lcInstallerInstallLog)
+ << "Unexpected checksum for file" << file.fileName();
+ return false;
+ }
+ }
+ return true;
+}
+
/*!
Constructs a new metadata object.
*/
@@ -122,18 +176,23 @@ QDomDocument Metadata::updatesDocument() const
}
/*!
- Returns \c true if the \c Updates.xml document of this metadata
- exists, \c false otherwise.
+ Returns \c true if the \c Updates.xml document of this metadata exists, and that all
+ meta files referenced in the document exist. If the \c Updates.xml contains a \c Checksum
+ element with a value of \c true, the integrity of the files is also verified.
+
+ Returns \c false otherwise.
*/
bool Metadata::isValid() const
{
- const QString updateFile(path() + QLatin1String("/Updates.xml"));
- if (!QFileInfo::exists(updateFile)) {
+ QFile updateFile(path() + QLatin1String("/Updates.xml"));
+ if (!updateFile.open(QIODevice::ReadOnly)) {
qCWarning(QInstaller::lcInstallerInstallLog)
- << "File" << updateFile << "does not exist.";
+ << "Cannot open" << updateFile.fileName()
+ << "for reading:" << updateFile.errorString();
return false;
}
- return true;
+
+ return verifyMetaFiles(&updateFile);
}
/*!
@@ -282,4 +341,78 @@ bool Metadata::containsRepositoryUpdates() const
return false;
}
+/*!
+ Verifies that the files referenced in \a updateFile document exist
+ on disk. If the document contains a \c Checksum element with a value
+ of \c true, the integrity of the files is also verified.
+
+ Returns \c true if the meta files are valid, \c false otherwise.
+*/
+bool Metadata::verifyMetaFiles(QFile *updateFile) const
+{
+ QDomDocument doc;
+ QString errorString;
+ if (!doc.setContent(updateFile, &errorString)) {
+ qCWarning(QInstaller::lcInstallerInstallLog)
+ << "Cannot set document content:" << errorString;
+ return false;
+ }
+
+ const QDomElement rootElement = doc.documentElement();
+ const QDomNodeList childNodes = rootElement.childNodes();
+
+ bool testChecksum = true;
+ const QDomElement checksumElement = rootElement.firstChildElement(QLatin1String("Checksum"));
+ if (!checksumElement.isNull())
+ testChecksum = (checksumElement.text().toLower() == scTrue);
+
+ for (int i = 0; i < childNodes.count(); ++i) {
+ const QDomElement element = childNodes.at(i).toElement();
+ if (element.isNull() || element.tagName() != QLatin1String("PackageUpdate"))
+ continue;
+
+ const QDomNodeList c2 = element.childNodes();
+ QString packageName;
+ QString unused1;
+ QString unused2;
+
+ // Only need the package name, so values for "online" and "testCheckSum" do not matter
+ if (!MetadataJob::parsePackageUpdate(c2, packageName, unused1, unused2, true, true))
+ continue; // nothing to check for this package
+
+ const QString packagePath = QString::fromLatin1("%1/%2/").arg(path(), packageName);
+ for (auto &metaTagName : qAsConst(*scMetaElements)) {
+ const QDomElement metaElement = element.firstChildElement(metaTagName);
+ if (metaElement.isNull())
+ continue;
+
+ if (metaElement.tagName() == QLatin1String("Licenses")) {
+ if (!verifyFileIntegrityFromElement(metaElement, QLatin1String("License"),
+ QLatin1String("file"), packagePath, testChecksum)) {
+ return false;
+ }
+ } else if (metaElement.tagName() == QLatin1String("UserInterfaces")) {
+ if (!verifyFileIntegrityFromElement(metaElement, QLatin1String("UserInterface"),
+ QString(), packagePath, testChecksum)) {
+ return false;
+ }
+ } else if (metaElement.tagName() == QLatin1String("Translations")) {
+ if (!verifyFileIntegrityFromElement(metaElement, QLatin1String("Translation"),
+ QString(), packagePath, testChecksum)) {
+ return false;
+ }
+ } else if (metaElement.tagName() == QLatin1String("Script")) {
+ if (!verifyFileIntegrityFromElement(metaElement.parentNode().toElement(),
+ QLatin1String("Script"), QString(), packagePath, testChecksum)) {
+ return false;
+ }
+ } else {
+ Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown meta element.");
+ }
+ }
+ }
+
+ return true;
+}
+
} // namespace QInstaller
diff --git a/src/libs/installer/metadata.h b/src/libs/installer/metadata.h
index 3063be829..7312d106b 100644
--- a/src/libs/installer/metadata.h
+++ b/src/libs/installer/metadata.h
@@ -35,6 +35,8 @@
#include <QDomDocument>
+class QFile;
+
namespace QInstaller {
class INSTALLER_EXPORT Metadata : public CacheableItem
@@ -64,6 +66,9 @@ public:
bool containsRepositoryUpdates() const;
private:
+ bool verifyMetaFiles(QFile *updateFile) const;
+
+private:
Repository m_repository;
QString m_persistentRepositoryPath;
mutable QByteArray m_checksum;
diff --git a/src/libs/installer/metadatajob.cpp b/src/libs/installer/metadatajob.cpp
index ff15d7b0d..663b209f7 100644
--- a/src/libs/installer/metadatajob.cpp
+++ b/src/libs/installer/metadatajob.cpp
@@ -193,7 +193,7 @@ bool MetadataJob::resetCache(bool init)
m_metaFromCache.setPath(m_core->settings().localCachePath());
m_metaFromCache.setType(QLatin1String("Metadata"));
- m_metaFromCache.setVersion(QLatin1String(QUOTE(IFW_REPOSITORY_FORMAT_VERSION)));
+ m_metaFromCache.setVersion(QLatin1String(QUOTE(IFW_CACHE_FORMAT_VERSION)));
if (!init)
return true;
@@ -630,6 +630,7 @@ void MetadataJob::metadataTaskFinished()
UnzipArchiveTask *task = new UnzipArchiveTask(result.target(),
item.value(TaskRole::UserRole).toString());
task->setRemoveArchive(true);
+ task->setStoreChecksums(true);
QFutureWatcher<void> *watcher = new QFutureWatcher<void>();
m_unzipTasks.insert(watcher, qobject_cast<QObject*> (task));
diff --git a/src/libs/installer/metadatajob_p.h b/src/libs/installer/metadatajob_p.h
index 5340e5d1c..8ab2d9e7d 100644
--- a/src/libs/installer/metadatajob_p.h
+++ b/src/libs/installer/metadatajob_p.h
@@ -62,11 +62,16 @@ class UnzipArchiveTask : public AbstractTask<void>
public:
UnzipArchiveTask(const QString &arcive, const QString &target)
- : m_archive(arcive), m_targetDir(target), m_removeArchive(false)
+ : m_archive(arcive)
+ , m_targetDir(target)
+ , m_removeArchive(false)
+ , m_storeChecksums(false)
{}
+
QString target() { return m_targetDir; }
QString archive() { return m_archive; }
void setRemoveArchive(bool remove) { m_removeArchive = remove; }
+ void setStoreChecksums(bool store) { m_storeChecksums = store; }
void doTask(QFutureInterface<void> &fi) override
{
@@ -82,12 +87,43 @@ public:
if (!archive) {
fi.reportException(UnzipArchiveException(MetadataJob::tr("Unsupported archive \"%1\": no handler "
"registered for file suffix \"%2\".").arg(m_archive, QFileInfo(m_archive).suffix())));
+ return;
} else if (!archive->open(QIODevice::ReadOnly)) {
fi.reportException(UnzipArchiveException(MetadataJob::tr("Cannot open file \"%1\" for "
"reading: %2").arg(QDir::toNativeSeparators(m_archive), archive->errorString())));
+ return;
} else if (!archive->extract(m_targetDir)) {
fi.reportException(UnzipArchiveException(MetadataJob::tr("Error while extracting "
"archive \"%1\": %2").arg(QDir::toNativeSeparators(m_archive), archive->errorString())));
+ return;
+ }
+
+ if (m_storeChecksums) {
+ // Calculate and store checksums of extracted files for later use
+ const QVector<ArchiveEntry> entries = archive->list();
+ for (auto &entry : entries) {
+ if (entry.isDirectory)
+ continue;
+
+ QFile file(m_targetDir + QDir::separator() + entry.path);
+ if (!file.open(QIODevice::ReadOnly)) {
+ fi.reportException(UnzipArchiveException(MetadataJob::tr("Cannot open extracted file \"%1\" for "
+ "reading: %2").arg(QDir::toNativeSeparators(file.fileName()), file.errorString())));
+ break;
+ }
+ QCryptographicHash hash(QCryptographicHash::Sha1);
+ hash.addData(&file);
+
+ const QByteArray hexChecksum = hash.result().toHex();
+ QFile hashFile(file.fileName() + QLatin1String(".sha1"));
+ if (!hashFile.open(QIODevice::WriteOnly)) {
+ fi.reportException(UnzipArchiveException(MetadataJob::tr("Cannot open file \"%1\" for "
+ "writing: %2").arg(QDir::toNativeSeparators(hashFile.fileName()), hashFile.errorString())));
+ break;
+ }
+ QTextStream stream(&hashFile);
+ stream << hexChecksum;
+ }
}
archive->close();
@@ -101,6 +137,7 @@ private:
QString m_archive;
QString m_targetDir;
bool m_removeArchive;
+ bool m_storeChecksums;
};
class CacheTaskException : public QException