From 9140c260ee8ab9a11cf5716a1e3218bdb4873099 Mon Sep 17 00:00:00 2001 From: derrod Date: Sun, 31 Jul 2022 18:04:55 +0200 Subject: [PATCH] UI: Add Whats New for macOS/Linux - Requires MbedTLS on Linux - Enabled by default on macOS and Flatpak - Enabled on linux via ENABLE_WHATSNEW_LINUX - Enables compilation of blake2 on Linux/macOS - Makes header name check also work with lowercase header - Changes WahtsNew to be only enabled when browser panels are available --- UI/CMakeLists.txt | 46 ++++ UI/data/OBSPublicRSAKey.pem | 14 ++ UI/nix-update/crypto-helpers-mac.mm | 32 +++ UI/nix-update/crypto-helpers-mbedtls.cpp | 39 ++++ UI/nix-update/crypto-helpers.hpp | 8 + UI/nix-update/nix-update-helpers.cpp | 27 +++ UI/nix-update/nix-update-helpers.hpp | 6 + UI/nix-update/nix-update.cpp | 282 +++++++++++++++++++++++ UI/nix-update/nix-update.hpp | 30 +++ UI/obs-app.cpp | 2 + UI/remote-text.cpp | 4 +- UI/window-basic-main.cpp | 16 +- deps/CMakeLists.txt | 2 +- 13 files changed, 496 insertions(+), 12 deletions(-) create mode 100755 UI/data/OBSPublicRSAKey.pem create mode 100644 UI/nix-update/crypto-helpers-mac.mm create mode 100644 UI/nix-update/crypto-helpers-mbedtls.cpp create mode 100644 UI/nix-update/crypto-helpers.hpp create mode 100644 UI/nix-update/nix-update-helpers.cpp create mode 100644 UI/nix-update/nix-update-helpers.hpp create mode 100644 UI/nix-update/nix-update.cpp create mode 100644 UI/nix-update/nix-update.hpp diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 62a4986b9..7ee4c9497 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -311,6 +311,18 @@ if(TARGET OBS::browser-panels) if(RESTREAM_ENABLED) target_sources(obs PRIVATE auth-restream.cpp auth-restream.hpp) endif() + + if(OS_WINDOWS OR OS_MACOS) + set(ENABLE_WHATSNEW + ON + CACHE INTERNAL "Enable WhatsNew dialog") + elseif(OS_LINUX) + option(ENABLE_WHATSNEW "Enable WhatsNew dialog" ON) + endif() + + if(ENABLE_WHATSNEW) + target_compile_definitions(obs PRIVATE WHATSNEW_ENABLED) + endif() endif() if(YOUTUBE_ENABLED) @@ -427,6 +439,21 @@ elseif(OS_MACOS) target_sources(obs PRIVATE forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) + if(ENABLE_WHATSNEW) + find_library(SECURITY Security) + mark_as_advanced(SECURITY) + target_link_libraries(obs PRIVATE ${SECURITY} OBS::blake2) + + target_sources( + obs + PRIVATE nix-update/crypto-helpers.hpp + nix-update/crypto-helpers-mac.mm + nix-update/nix-update.cpp + nix-update/nix-update.hpp + nix-update/nix-update-helpers.cpp + nix-update/nix-update-helpers.hpp) + endif() + set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) @@ -447,6 +474,25 @@ elseif(OS_POSIX) if(OS_FREEBSD) target_link_libraries(obs PRIVATE procstat) endif() + + if(OS_LINUX AND ENABLE_WHATSNEW) + find_package(MbedTLS) + if(NOT MBEDTLS_FOUND) + obs_status( + FATAL_ERROR + "mbedTLS not found, but required for WhatsNew support on Linux") + endif() + + target_sources( + obs + PRIVATE nix-update/crypto-helpers.hpp + nix-update/crypto-helpers-mbedtls.cpp + nix-update/nix-update.cpp + nix-update/nix-update.hpp + nix-update/nix-update-helpers.cpp + nix-update/nix-update-helpers.hpp) + target_link_libraries(obs PRIVATE Mbedtls::Mbedtls OBS::blake2) + endif() endif() get_target_property(_SOURCES obs SOURCES) diff --git a/UI/data/OBSPublicRSAKey.pem b/UI/data/OBSPublicRSAKey.pem new file mode 100755 index 000000000..e2707afb7 --- /dev/null +++ b/UI/data/OBSPublicRSAKey.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAl3sverw9HQ+rYQNn9Ca7 +9LU62nG6NozE/FsIVNer+W/hueE8WQ1mrFP+2yA+iYRutYPeEgpxtodHhgkR4pEK +Vnrr18q4szlv89QI7t8lMoLTlF+t1IR0V4wJV34I3C+359KixnzL0Bl9aj/zDcrX +Wl5pHTioJwYOgMiBGLyPeitMFdjjTIpCM+mxTWXCrZ9dPUKvZtgzjd+IzlHidHtO +ORBN5mRs8LNO58k79r77DcgQYPNiiCtWgC+Y4K7uSZX3Hveom2tHbVXy0L/Cl7fM +HKqfcQGuyrvud42OrWarAsn2p2Ei6Kzxb3G6ESCw15nHAgLal8zSq7+raE/xkLpC +bYg5gmY6vbmWnq9dqWrUzaqOfrZPgvgG0WvkBShfaEOBaIUxA3QBgzAZhqeedF9h +afMGMM9qVbfwuuzJ2uh+InaGaeH2c04oVcDFfeOaDuxRjCCbqr5sLSo1CWokynjN +CB+b2rQF7DPPbD4s/nT9Nsck/NFzrBXRO+dqkeBwDUCv7bZgW7OxuOX07LTqfp5s +OeGgububiwY3UdHYq+L9JqISG1tM4HeKjaHju1MDjvHZ2DbmLwUxuYa6JZDKWs7r +IrdDwx3JwacF66h3YUW6tzUZhztcmQepP/u7BgGrkOPPpYA0NEJ80SeAx7hiN4va +eEQKnRn+EpBN6UBa5f7LoK8CAwEAAQ== +-----END PUBLIC KEY----- diff --git a/UI/nix-update/crypto-helpers-mac.mm b/UI/nix-update/crypto-helpers-mac.mm new file mode 100644 index 000000000..75ee0dc84 --- /dev/null +++ b/UI/nix-update/crypto-helpers-mac.mm @@ -0,0 +1,32 @@ +#include "crypto-helpers.hpp" + +#import +#import +#import + +bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, + const uint8_t *buf, const size_t len, const uint8_t *sig, + const size_t sigLen) +{ + NSData *pubKeyData = [NSData dataWithBytes:pubKey length:pubKeyLen]; + CFArrayRef items = nullptr; + + OSStatus res = SecItemImport((CFDataRef)pubKeyData, nullptr, nullptr, + nullptr, (SecItemImportExportFlags)0, + nullptr, nullptr, &items); + if (res != errSecSuccess) + return false; + + SecKeyRef pubKeyRef = (SecKeyRef)CFArrayGetValueAtIndex(items, 0); + NSData *signedData = [NSData dataWithBytes:buf length:len]; + NSData *signature = [NSData dataWithBytes:sig length:sigLen]; + + CFErrorRef errRef; + bool result = SecKeyVerifySignature( + pubKeyRef, kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA512, + (__bridge CFDataRef)signedData, (__bridge CFDataRef)signature, + &errRef); + + CFRelease(items); + return result; +}; diff --git a/UI/nix-update/crypto-helpers-mbedtls.cpp b/UI/nix-update/crypto-helpers-mbedtls.cpp new file mode 100644 index 000000000..95088bdbd --- /dev/null +++ b/UI/nix-update/crypto-helpers-mbedtls.cpp @@ -0,0 +1,39 @@ +#include "crypto-helpers.hpp" + +#include "mbedtls/md.h" +#include "mbedtls/pk.h" + +bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, + const uint8_t *buf, const size_t len, const uint8_t *sig, + const size_t sigLen) +{ + bool result = false; + int ret = 1; + unsigned char hash[64]; + mbedtls_pk_context pk; + + mbedtls_pk_init(&pk); + + // Parse PEM key + if ((ret = mbedtls_pk_parse_public_key(&pk, pubKey, pubKeyLen + 1)) != + 0) { + goto exit; + } + // Hash input buffer + if ((ret = mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA512), buf, + len, hash)) != 0) { + goto exit; + } + // Verify signautre + if ((ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA512, hash, 64, sig, + sigLen)) != 0) { + goto exit; + } + + result = true; + +exit: + mbedtls_pk_free(&pk); + + return result; +} diff --git a/UI/nix-update/crypto-helpers.hpp b/UI/nix-update/crypto-helpers.hpp new file mode 100644 index 000000000..ebf94f0d9 --- /dev/null +++ b/UI/nix-update/crypto-helpers.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +bool VerifySignature(const uint8_t *pubKey, const size_t pubKeyLen, + const uint8_t *buf, const size_t len, const uint8_t *sig, + const size_t sigLen); diff --git a/UI/nix-update/nix-update-helpers.cpp b/UI/nix-update/nix-update-helpers.cpp new file mode 100644 index 000000000..69fb20cbd --- /dev/null +++ b/UI/nix-update/nix-update-helpers.cpp @@ -0,0 +1,27 @@ +#include "nix-update-helpers.hpp" + +#include + +std::string vstrprintf(const char *format, va_list args) +{ + if (!format) + return std::string(); + + std::string str; + int size = (int)vsnprintf(nullptr, 0, format, args) + 1; + str.resize(size); + vsnprintf(&str[0], size, format, args); + return str; +} + +std::string strprintf(const char *format, ...) +{ + std::string str; + va_list args; + + va_start(args, format); + str = vstrprintf(format, args); + va_end(args); + + return str; +} diff --git a/UI/nix-update/nix-update-helpers.hpp b/UI/nix-update/nix-update-helpers.hpp new file mode 100644 index 000000000..14d6e2c97 --- /dev/null +++ b/UI/nix-update/nix-update-helpers.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +std::string strprintf(const char *format, ...); diff --git a/UI/nix-update/nix-update.cpp b/UI/nix-update/nix-update.cpp new file mode 100644 index 000000000..64f580bd1 --- /dev/null +++ b/UI/nix-update/nix-update.cpp @@ -0,0 +1,282 @@ +#include "nix-update.hpp" +#include "crypto-helpers.hpp" +#include "nix-update-helpers.hpp" +#include "obs-app.hpp" +#include "remote-text.hpp" +#include "platform.hpp" + +#include +#include + +#include +#include + +#include +#include +#include + +#include + +struct QCef; +extern QCef *cef; + +#ifndef MAC_WHATSNEW_URL +#define MAC_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + +#ifndef LINUX_WHATSNEW_URL +#define LINUX_WHATSNEW_URL "https://obsproject.com/update_studio/whatsnew.json" +#endif + +#ifdef __APPLE__ +#define WHATSNEW_URL MAC_WHATSNEW_URL +#else +#define WHATSNEW_URL LINUX_WHATSNEW_URL +#endif + +#define HASH_READ_BUF_SIZE 65536 +#define BLAKE2_HASH_LENGTH 20 + +/* ------------------------------------------------------------------------ */ + +static bool QuickWriteFile(const char *file, std::string &data) +try { + std::ofstream fileStream(file, std::ios::binary); + if (fileStream.fail()) + throw strprintf("Failed to open file '%s': %s", file, + strerror(errno)); + + fileStream.write(data.data(), data.size()); + if (fileStream.fail()) + throw strprintf("Failed to write file '%s': %s", file, + strerror(errno)); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static bool QuickReadFile(const char *file, std::string &data) +try { + std::ifstream fileStream(file); + if (!fileStream.is_open() || fileStream.fail()) + throw strprintf("Failed to open file '%s': %s", file, + strerror(errno)); + + fileStream.seekg(0, fileStream.end); + size_t size = fileStream.tellg(); + fileStream.seekg(0); + + data.resize(size); + fileStream.read(&data[0], size); + + if (fileStream.fail()) + throw strprintf("Failed to write file '%s': %s", file, + strerror(errno)); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +static bool CalculateFileHash(const char *path, uint8_t *hash) +try { + blake2b_state blake2; + if (blake2b_init(&blake2, BLAKE2_HASH_LENGTH) != 0) + return false; + + std::ifstream file(path, std::ios::binary); + if (!file.is_open() || file.fail()) + return false; + + char buf[HASH_READ_BUF_SIZE]; + + for (;;) { + file.read(buf, HASH_READ_BUF_SIZE); + size_t read = file.gcount(); + if (blake2b_update(&blake2, &buf, read) != 0) + return false; + if (file.eof()) + break; + } + + if (blake2b_final(&blake2, hash, BLAKE2_HASH_LENGTH) != 0) + return false; + + return true; + +} catch (std::string &text) { + blog(LOG_DEBUG, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +void GenerateGUID(std::string &guid) +{ + const char alphabet[] = "0123456789abcdef"; + QRandomGenerator *rng = QRandomGenerator::system(); + + guid.resize(40); + + for (size_t i = 0; i < 40; i++) { + guid[i] = alphabet[rng->bounded(0, 16)]; + } +} + +std::string GetProgramGUID() +{ + static std::mutex m; + std::lock_guard lock(m); + + /* NOTE: this is an arbitrary random number that we use to count the + * number of unique OBS installations and is not associated with any + * kind of identifiable information */ + const char *pguid = + config_get_string(GetGlobalConfig(), "General", "InstallGUID"); + std::string guid; + if (pguid) + guid = pguid; + + if (guid.empty()) { + GenerateGUID(guid); + + if (!guid.empty()) + config_set_string(GetGlobalConfig(), "General", + "InstallGUID", guid.c_str()); + } + + return guid; +} + +/* ------------------------------------------------------------------------ */ + +static void LoadPublicKey(std::string &pubkey) +{ + std::string pemFilePath; + + if (!GetDataFilePath("OBSPublicRSAKey.pem", pemFilePath)) + throw std::string("Could not find OBS public key file!"); + if (!QuickReadFile(pemFilePath.c_str(), pubkey)) + throw std::string("Could not read OBS public key file!"); +} + +static bool CheckDataSignature(const char *name, const std::string &data, + const std::string &hexSig) +try { + if (hexSig.empty() || hexSig.length() > 0xFFFF || + (hexSig.length() & 1) != 0) + throw strprintf("Missing or invalid signature for %s: %s", name, + hexSig.c_str()); + + static std::string obsPubKey; + if (obsPubKey.empty()) + LoadPublicKey(obsPubKey); + + // Convert hex string to bytes + auto signature = QByteArray::fromHex(hexSig.data()); + + if (!VerifySignature((uint8_t *)obsPubKey.data(), obsPubKey.size(), + (uint8_t *)data.data(), data.size(), + (uint8_t *)signature.data(), signature.size())) + throw strprintf("Signature check failed for %s", name); + + return true; + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); + return false; +} + +/* ------------------------------------------------------------------------ */ + +void WhatsNewInfoThread::run() +try { + long responseCode; + std::vector extraHeaders; + std::string text; + std::string error; + std::string signature; + uint8_t whatsnewHash[BLAKE2_HASH_LENGTH]; + bool success; + + BPtr whatsnewPath = + GetConfigPathPtr("obs-studio/updates/whatsnew.json"); + + /* ----------------------------------- * + * avoid downloading json again */ + + if (CalculateFileHash(whatsnewPath, whatsnewHash)) { + auto hash = QByteArray::fromRawData((const char *)whatsnewHash, + BLAKE2_HASH_LENGTH); + + QString header = "If-None-Match: " + hash.toHex(); + extraHeaders.push_back(move(header.toStdString())); + } + + /* ----------------------------------- * + * get current install GUID */ + + std::string guid = GetProgramGUID(); + + if (!guid.empty()) { + std::string header = "X-OBS2-GUID: " + guid; + extraHeaders.push_back(move(header)); + } + + /* ----------------------------------- * + * get json from server */ + + success = GetRemoteFile(WHATSNEW_URL, text, error, &responseCode, + nullptr, "", nullptr, extraHeaders, &signature); + + if (!success || (responseCode != 200 && responseCode != 304)) { + if (responseCode == 404) + return; + + throw strprintf("Failed to fetch whatsnew file: %s", + error.c_str()); + } + + /* ----------------------------------- * + * verify file signature */ + + if (responseCode == 200) { + success = CheckDataSignature("whatsnew", text, signature); + if (!success) + throw std::string("Invalid whatsnew signature"); + } + + /* ----------------------------------- * + * write or load json */ + + if (responseCode == 200) { + if (!QuickWriteFile(whatsnewPath, text)) + throw strprintf("Could not write file '%s'", + whatsnewPath.Get()); + } else { + if (!QuickReadFile(whatsnewPath, text)) + throw strprintf("Could not read file '%s'", + whatsnewPath.Get()); + } + + /* ----------------------------------- * + * success */ + + emit Result(QString::fromStdString(text)); + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} + +/* ------------------------------------------------------------------------ */ + +void WhatsNewBrowserInitThread::run() +{ + cef->wait_for_browser_init(); + emit Result(url); +} diff --git a/UI/nix-update/nix-update.hpp b/UI/nix-update/nix-update.hpp new file mode 100644 index 000000000..56394930f --- /dev/null +++ b/UI/nix-update/nix-update.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +class WhatsNewInfoThread : public QThread { + Q_OBJECT + + virtual void run() override; + +signals: + void Result(const QString &text); + +public: + inline WhatsNewInfoThread() {} +}; + +class WhatsNewBrowserInitThread : public QThread { + Q_OBJECT + + QString url; + + virtual void run() override; + +signals: + void Result(const QString &url); + +public: + inline WhatsNewBrowserInitThread(const QString &url_) : url(url_) {} +}; diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 5cccc2415..1e13b6748 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -551,7 +551,9 @@ static bool MakeUserDirs() return false; if (!do_mkdir(path)) return false; +#endif +#ifdef WHATSNEW_ENABLED if (GetConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) return false; if (!do_mkdir(path)) diff --git a/UI/remote-text.cpp b/UI/remote-text.cpp index 83afa9a53..c99d356db 100644 --- a/UI/remote-text.cpp +++ b/UI/remote-text.cpp @@ -208,7 +208,9 @@ bool GetRemoteFile(const char *url, std::string &str, std::string &error, } else if (signature) { for (string &h : header_in_list) { string name = h.substr(0, 13); - if (name == "X-Signature: ") { + // HTTP headers are technically case-insensitive + if (name == "X-Signature: " || + name == "x-signature: ") { *signature = h.substr(13); break; } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 4880651e0..8adf7b19f 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -81,6 +81,10 @@ #include "windows.h" #endif +#if !defined(_WIN32) && defined(WHATSNEW_ENABLED) +#include "nix-update/nix-update.hpp" +#endif + #include "ui_OBSBasic.h" #include "ui_ColorSelect.h" @@ -2106,7 +2110,7 @@ void OBSBasic::OnFirstLoad() if (api) api->on_event(OBS_FRONTEND_EVENT_FINISHED_LOADING); -#if defined(BROWSER_AVAILABLE) && defined(_WIN32) +#ifdef WHATSNEW_ENABLED /* Attempt to load init screen if available */ if (cef) { WhatsNewInfoThread *wnit = new WhatsNewInfoThread(); @@ -2141,8 +2145,7 @@ void OBSBasic::OnFirstLoad() /* shows a "what's new" page on startup of new versions using CEF */ void OBSBasic::ReceivedIntroJson(const QString &text) { -#ifdef BROWSER_AVAILABLE -#ifdef _WIN32 +#ifdef WHATSNEW_ENABLED if (closing) return; @@ -2229,9 +2232,6 @@ void OBSBasic::ReceivedIntroJson(const QString &text) whatsNewInitThread.reset(wnbit); whatsNewInitThread->start(); -#else - UNUSED_PARAMETER(text); -#endif #else UNUSED_PARAMETER(text); #endif @@ -2243,7 +2243,6 @@ void OBSBasic::ReceivedIntroJson(const QString &text) void OBSBasic::ShowWhatsNew(const QString &url) { #ifdef BROWSER_AVAILABLE -#ifdef _WIN32 if (closing) return; @@ -2282,9 +2281,6 @@ void OBSBasic::ShowWhatsNew(const QString &url) #else UNUSED_PARAMETER(url); #endif -#else - UNUSED_PARAMETER(url); -#endif } void OBSBasic::UpdateMultiviewProjectorMenu() diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 6eb263ce5..0766bdb37 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -4,10 +4,10 @@ if(OS_WINDOWS) endif() add_subdirectory(ipc-util) - add_subdirectory(blake2) add_subdirectory(lzma) endif() +add_subdirectory(blake2) add_subdirectory(glad) add_subdirectory(media-playback) add_subdirectory(file-updater)