ソースを参照

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
derrod 3 年 前
コミット
9140c260ee

+ 46 - 0
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)

+ 14 - 0
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-----

+ 32 - 0
UI/nix-update/crypto-helpers-mac.mm

@@ -0,0 +1,32 @@
+#include "crypto-helpers.hpp"
+
+#import <Foundation/Foundation.h>
+#import <Security/Security.h>
+#import <Security/SecKey.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)
+{
+	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;
+};

+ 39 - 0
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;
+}

+ 8 - 0
UI/nix-update/crypto-helpers.hpp

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <stdlib.h>
+#include <cstdint>
+
+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);

+ 27 - 0
UI/nix-update/nix-update-helpers.cpp

@@ -0,0 +1,27 @@
+#include "nix-update-helpers.hpp"
+
+#include <stdarg.h>
+
+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;
+}

+ 6 - 0
UI/nix-update/nix-update-helpers.hpp

@@ -0,0 +1,6 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+std::string strprintf(const char *format, ...);

+ 282 - 0
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 <util/util.hpp>
+#include <blake2.h>
+
+#include <iostream>
+#include <fstream>
+
+#include <QRandomGenerator>
+#include <QByteArray>
+#include <QString>
+
+#include <browser-panel.hpp>
+
+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<std::mutex> 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<std::string> extraHeaders;
+	std::string text;
+	std::string error;
+	std::string signature;
+	uint8_t whatsnewHash[BLAKE2_HASH_LENGTH];
+	bool success;
+
+	BPtr<char> 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);
+}

+ 30 - 0
UI/nix-update/nix-update.hpp

@@ -0,0 +1,30 @@
+#pragma once
+
+#include <QThread>
+#include <QString>
+
+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_) {}
+};

+ 2 - 0
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))

+ 3 - 1
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;
 				}

+ 6 - 10
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()

+ 1 - 1
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)