diff --git a/components/UpdateDialog.qml b/components/UpdateDialog.qml index 74c62516..5fbef79e 100644 --- a/components/UpdateDialog.qml +++ b/components/UpdateDialog.qml @@ -37,11 +37,12 @@ import "../components" as MoneroComponents Popup { id: updateDialog + property bool active: false property bool allowed: true property string error: "" property string filename: "" + property string hash: "" property double progress: url && downloader.total > 0 ? downloader.loaded * 100 / downloader.total : 0 - property bool active: false property string url: "" property bool valid: false property string version: "" @@ -55,8 +56,9 @@ Popup { padding: 20 visible: active && allowed - function show(version, url) { + function show(version, url, hash) { updateDialog.error = ""; + updateDialog.hash = hash; updateDialog.url = url; updateDialog.valid = false; updateDialog.version = version; @@ -86,7 +88,7 @@ Popup { Text { id: statusText - color: MoneroComponents.Style.defaultFontColor + color: updateDialog.valid ? MoneroComponents.Style.green : MoneroComponents.Style.defaultFontColor font.family: MoneroComponents.Style.fontRegular.name font.pixelSize: 18 visible: !errorText.visible @@ -102,9 +104,9 @@ Popup { + translationManager.emptyString; } if (updateDialog.valid) { - return qsTr("Download finished") + translationManager.emptyString; + return qsTr("Update downloaded, signature verified") + translationManager.emptyString; } - return qsTr("Do you want to download new version?") + translationManager.emptyString; + return qsTr("Do you want to download and verify new version?") + translationManager.emptyString; } } @@ -156,8 +158,9 @@ Popup { onClicked: { updateDialog.error = ""; updateDialog.filename = updateDialog.url.replace(/^.*\//, ''); - const downloadingStarted = downloader.get(updateDialog.url, function(error) { + const downloadingStarted = downloader.get(updateDialog.url, updateDialog.hash, function(error) { if (error) { + console.error("Download failed", error); updateDialog.error = qsTr("Download failed") + translationManager.emptyString; } else { updateDialog.valid = true; diff --git a/main.qml b/main.qml index 694aee27..a3651137 100644 --- a/main.qml +++ b/main.qml @@ -1972,15 +1972,11 @@ ApplicationWindow { closeWallet(Qt.quit); } - function onWalletCheckUpdatesComplete(update) { - if (update === "") - return - print("Update found: " + update) - var parts = update.split("|") - if (parts.length == 4) { - updateDialog.show(parts[0], isMac || isWindows || isLinux ? parts[3] : ""); - } else { - print("Failed to parse update spec") + function onWalletCheckUpdatesComplete(version, downloadUrl, hash, firstSigner, secondSigner) { + const alreadyAsked = updateDialog.url == downloadUrl && updateDialog.hash == hash; + if (!alreadyAsked) + { + updateDialog.show(version, isMac || isWindows || isLinux ? downloadUrl : "", hash); } } diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index 01faf905..ed4f5776 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -41,6 +41,8 @@ #include #include +#include "qt/updater.h" + class WalletPassphraseListenerImpl : public Monero::WalletListener { public: @@ -464,12 +466,32 @@ bool WalletManager::saveQrCode(const QString &code, const QString &path) const void WalletManager::checkUpdatesAsync(const QString &software, const QString &subdir) { m_scheduler.run([this, software, subdir] { - emit checkUpdatesComplete(checkUpdates(software, subdir)); + const auto updateInfo = Monero::WalletManager::checkUpdates(software.toStdString(), subdir.toStdString()); + if (!std::get<0>(updateInfo)) + { + return; + } + + const QString version = QString::fromStdString(std::get<1>(updateInfo)); + const QByteArray hashFromDns = QByteArray::fromHex(QString::fromStdString(std::get<2>(updateInfo)).toUtf8()); + const QString downloadUrl = QString::fromStdString(std::get<4>(updateInfo)); + + try + { + const QString binaryFilename = QUrl(downloadUrl).fileName(); + QPair signers; + const QString signedHash = Updater().fetchSignedHash(binaryFilename, hashFromDns, signers).toHex(); + + qInfo() << "Update found" << version << downloadUrl << "hash" << signedHash << "signed by" << signers; + emit checkUpdatesComplete(version, downloadUrl, signedHash, signers.first, signers.second); + } + catch (const std::exception &e) + { + qCritical() << "Failed to fetch and verify signed hash:" << e.what(); + } }); } - - QString WalletManager::checkUpdates(const QString &software, const QString &subdir) const { qDebug() << "Checking for updates"; diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index d03e4e13..6470152d 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -36,7 +36,6 @@ #include #include #include -#include #include "qt/FutureScheduler.h" #include "NetworkType.h" @@ -192,7 +191,12 @@ signals: void walletPassphraseNeeded(); void deviceButtonRequest(quint64 buttonCode); void deviceButtonPressed(); - void checkUpdatesComplete(const QString &result) const; + void checkUpdatesComplete( + const QString &version, + const QString &downloadUrl, + const QString &hash, + const QString &firstSigner, + const QString &secondSigner) const; void miningStatus(bool isMining) const; public slots: diff --git a/src/qt/downloader.cpp b/src/qt/downloader.cpp index afd2049e..d925c77c 100644 --- a/src/qt/downloader.cpp +++ b/src/qt/downloader.cpp @@ -31,6 +31,8 @@ #include #include +#include "updater.h" + namespace { @@ -112,10 +114,10 @@ void Downloader::cancel() m_contents.clear(); } -bool Downloader::get(const QString &url, const QJSValue &callback) +bool Downloader::get(const QString &url, const QString &hash, const QJSValue &callback) { auto future = m_scheduler.run( - [this, url]() { + [this, url, hash]() { DownloaderStateGuard stateGuard(m_active, m_mutex, [this]() { emit activeChanged(); }); @@ -153,6 +155,19 @@ bool Downloader::get(const QString &url, const QJSValue &callback) return QJSValueList({"empty response"}); } + try + { + const QByteArray calculatedHash = Updater().getHash(&response[0], response.size()); + if (QByteArray::fromHex(hash.toUtf8()) != calculatedHash) + { + return QJSValueList({"hash sum mismatch"}); + } + } + catch (const std::exception &e) + { + return QJSValueList({e.what()}); + } + { QWriteLocker locker(&m_mutex); diff --git a/src/qt/downloader.h b/src/qt/downloader.h index 44840535..418627a5 100644 --- a/src/qt/downloader.h +++ b/src/qt/downloader.h @@ -44,7 +44,7 @@ public: ~Downloader(); Q_INVOKABLE void cancel(); - Q_INVOKABLE bool get(const QString &url, const QJSValue &callback); + Q_INVOKABLE bool get(const QString &url, const QString &hash, const QJSValue &callback); Q_INVOKABLE bool saveToFile(const QString &path) const; signals: diff --git a/src/qt/network.cpp b/src/qt/network.cpp index 01d0f296..1a674508 100644 --- a/src/qt/network.cpp +++ b/src/qt/network.cpp @@ -117,6 +117,17 @@ void Network::getJSON(const QString &url, const QJSValue &callback) const get(url, callback, "application/json; charset=utf-8"); } +std::string Network::get(const QString &url, const QString &contentType /* = {} */) const +{ + std::string response; + QString error = get(std::shared_ptr(new http_simple_client()), url, response, contentType); + if (!error.isEmpty()) + { + throw std::runtime_error(QString("failed to fetch %1: %2").arg(url).arg(error).toStdString()); + } + return response; +} + QString Network::get( std::shared_ptr httpClient, const QString &url, diff --git a/src/qt/network.h b/src/qt/network.h index acdd6280..e741247d 100644 --- a/src/qt/network.h +++ b/src/qt/network.h @@ -77,6 +77,7 @@ public: Q_INVOKABLE void get(const QString &url, const QJSValue &callback, const QString &contentType = {}) const; Q_INVOKABLE void getJSON(const QString &url, const QJSValue &callback) const; + std::string get(const QString &url, const QString &contentType = {}) const; QString get( std::shared_ptr httpClient, const QString &url, diff --git a/src/qt/updater.cpp b/src/qt/updater.cpp index 5968c2b5..17d6eddb 100644 --- a/src/qt/updater.cpp +++ b/src/qt/updater.cpp @@ -30,6 +30,7 @@ #include +#include "network.h" #include "utils.h" Updater::Updater() @@ -39,17 +40,41 @@ Updater::Updater() m_maintainers.emplace_back(fileGetContents(":/monero/utils/gpg_keys/luigi1111.asc").toStdString()); } -QPair Updater::verifySignaturesAndHashSum( +QByteArray Updater::fetchSignedHash( + const QString &binaryFilename, + const QByteArray &hashFromDns, + QPair &signers) const +{ + static constexpr const char hashesTxtUrl[] = "https://web.getmonero.org/downloads/hashes.txt"; + static constexpr const char hashesTxtSigUrl[] = "https://web.getmonero.org/downloads/hashes.txt.sig"; + + const Network network; + std::string hashesTxt = network.get(hashesTxtUrl); + std::string hashesTxtSig = network.get(hashesTxtSigUrl); + + const QByteArray signedHash = verifyParseSignedHahes( + QByteArray(&hashesTxt[0], hashesTxt.size()), + QByteArray(&hashesTxtSig[0], hashesTxtSig.size()), + binaryFilename, + signers); + + if (signedHash != hashFromDns) + { + throw std::runtime_error("DNS hash mismatch"); + } + + return signedHash; +} + +QByteArray Updater::verifyParseSignedHahes( const QByteArray &armoredSignedHashes, const QByteArray &secondDetachedSignature, const QString &binaryFilename, - const void *binaryData, - size_t binarySize) const + QPair &signers) const { - QString firstSigner; - const QString signedMessage = verifySignature(armoredSignedHashes, firstSigner); + const QString signedMessage = verifySignature(armoredSignedHashes, signers.first); - QString secondSigner = verifySignature( + signers.second = verifySignature( epee::span( reinterpret_cast(armoredSignedHashes.data()), armoredSignedHashes.size()), @@ -57,19 +82,31 @@ QPair Updater::verifySignaturesAndHashSum( reinterpret_cast(secondDetachedSignature.data()), secondDetachedSignature.size()))); - if (firstSigner == secondSigner) + if (signers.first == signers.second) { throw std::runtime_error("both signatures were generated by the same person"); } - const QByteArray signedHash = parseShasumOutput(signedMessage, binaryFilename); + return parseShasumOutput(signedMessage, binaryFilename); +} + +QPair Updater::verifySignaturesAndHashSum( + const QByteArray &armoredSignedHashes, + const QByteArray &secondDetachedSignature, + const QString &binaryFilename, + const void *binaryData, + size_t binarySize) const +{ + QPair signers; + const QByteArray signedHash = + verifyParseSignedHahes(armoredSignedHashes, secondDetachedSignature, binaryFilename, signers); const QByteArray calculatedHash = getHash(binaryData, binarySize); if (signedHash != calculatedHash) { throw std::runtime_error("hash sum mismatch"); } - return {firstSigner, secondSigner}; + return signers; } QByteArray Updater::getHash(const void *data, size_t size) const diff --git a/src/qt/updater.h b/src/qt/updater.h index bc06d0c0..48a59a71 100644 --- a/src/qt/updater.h +++ b/src/qt/updater.h @@ -37,6 +37,11 @@ class Updater public: Updater(); + QByteArray fetchSignedHash( + const QString &binaryFilename, + const QByteArray &hashFromDns, + QPair &signers) const; + QByteArray getHash(const void *data, size_t size) const; QPair verifySignaturesAndHashSum( const QByteArray &armoredSignedHashes, const QByteArray &secondDetachedSignature, @@ -45,7 +50,11 @@ public: size_t binarySize) const; private: - QByteArray getHash(const void *data, size_t size) const; + QByteArray verifyParseSignedHahes( + const QByteArray &armoredSignedHashes, + const QByteArray &secondDetachedSignature, + const QString &binaryFilename, + QPair &signers) const; QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const; QString verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const; QByteArray parseShasumOutput(const QString &message, const QString &filename) const;