From eefc5bf2b7f1f123147fa55f8fc93541d8adafee Mon Sep 17 00:00:00 2001 From: sebaszm <45654185+sebaszm@users.noreply.github.com> Date: Tue, 3 Oct 2023 12:34:34 +0200 Subject: [PATCH] [WebKitBrowser] Abstract cookie jar crypto (#254) --- WebKitBrowser/CMakeLists.txt | 3 +- WebKitBrowser/CookieJar.cpp | 338 ++++++++++++++++++++----- WebKitBrowser/CookieJar.h | 3 + WebKitBrowser/WebKitBrowser.cpp | 14 + WebKitBrowser/WebKitImplementation.cpp | 79 +++++- 5 files changed, 368 insertions(+), 69 deletions(-) diff --git a/WebKitBrowser/CMakeLists.txt b/WebKitBrowser/CMakeLists.txt index ef4ea01e..61215f09 100644 --- a/WebKitBrowser/CMakeLists.txt +++ b/WebKitBrowser/CMakeLists.txt @@ -225,6 +225,7 @@ set(PLUGIN_AMAZON_MEMORYPRESSURE_NETWORKPROCESS_SETTINGS_POLLINTERVAL ${PLUGIN_W find_package(${NAMESPACE}Plugins REQUIRED) find_package(${NAMESPACE}Definitions REQUIRED) +find_package(${NAMESPACE}Cryptography REQUIRED) find_package(CompileSettingsDebug CONFIG REQUIRED) find_package(WPEWebKit REQUIRED) find_package(WPEBackend REQUIRED) @@ -261,6 +262,7 @@ target_link_libraries(${PLUGIN_WEBKITBROWSER_IMPLEMENTATION} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins ${NAMESPACE}Definitions::${NAMESPACE}Definitions + ${NAMESPACE}Cryptography::${NAMESPACE}Cryptography WPEBackend::WPEBackend WPEWebKit::WPEWebKit) @@ -273,7 +275,6 @@ if (PLUGIN_WEBKITBROWSER_CLOUD_COOKIEJAR) PRIVATE ENABLE_CLOUD_COOKIE_JAR=1) target_sources(${PLUGIN_WEBKITBROWSER_IMPLEMENTATION} PRIVATE CookieJar.cpp) - include(CookieJarCrypto/CMakeLists.txt) endif() if (PLUGIN_WEBKITBROWSER_LOGGING_UTILS) diff --git a/WebKitBrowser/CookieJar.cpp b/WebKitBrowser/CookieJar.cpp index 6f9719c0..7ea883a3 100644 --- a/WebKitBrowser/CookieJar.cpp +++ b/WebKitBrowser/CookieJar.cpp @@ -26,17 +26,16 @@ #include #include -#if defined(COOKIE_JAR_CRYPTO_IMPLEMENTATION) -#include COOKIE_JAR_CRYPTO_IMPLEMENTATION -#else -#error "Please define COOKIE_JAR_CRYPTO_IMPLEMENTATION" -#endif +#include + namespace WPEFramework { namespace Plugin { namespace { +static constexpr uint8_t PAYLOAD_VERSION = 1; + static std::string serialize(const std::vector& cookies) { std::ostringstream os; @@ -55,29 +54,6 @@ static void deserialize(const std::string& cookies, std::vector& re } } -static std::string toBase64(const std::vector& in) -{ - gchar* encoded = g_base64_encode(in.data(), in.size()); - std::string result; - if (encoded) { - result.assign(encoded); - g_free(encoded); - } - return result; -} - -static std::vector fromBase64(const std::string& str) -{ - gsize outlen; - guint8* decoded = g_base64_decode(str.c_str(), &outlen); - std::vector result; - if (decoded) { - result.assign(decoded, decoded + outlen); - g_free(decoded); - } - return result; -} - static std::vector compress(const std::string& str) { std::vector result; @@ -177,66 +153,304 @@ static uint32_t crc_checksum(const std::string& str) } // namespace -struct CookieJar::CookieJarPrivate +class CookieJar::CookieJarPrivate { - CookieJarCrypto _cookieJarCrypto; +public: + CookieJarPrivate() + : _lock() + , _connector() + , _key() + { + } + ~CookieJarPrivate() = default; + + CookieJarPrivate(const CookieJarPrivate&) = delete; + CookieJarPrivate& operator=(const CookieJarPrivate&) = delete; - uint32_t Pack(const std::vector &cookies, uint32_t& version, uint32_t& checksum, string& payload) +public: + uint32_t Configure(const string& connector, const string& key) { - uint32_t rc; - std::string serialized; - std::vector encrypted; + uint32_t rc = Core::ERROR_NONE; + + _lock.Lock(); - serialized = serialize(cookies); - checksum = crc_checksum(serialized); + if (key.empty() == false) { + ASSERT(connector.empty() == false); - rc = _cookieJarCrypto.Encrypt(compress(serialized), version, encrypted); + if ((_key != key) || (_connector != connector)) { - if (rc != Core::ERROR_NONE) { - TRACE_GLOBAL(Trace::Error,(_T("Encryption failed, rc = %u"), rc)); - } else { - payload = toBase64(encrypted); + _connector = connector; + _key = key; + + TRACE_GLOBAL(Trace::Information, (_T("CookieJar crypto configured (%s)"), _connector.c_str())); + } + } + else { + ASSERT(connector.empty() == true); + + _connector.clear(); + _key.clear(); + + TRACE_GLOBAL(Trace::Information, (_T("CookieJar crypto unconfigured"))); + } + + _lock.Unlock(); + + return (rc); + } + +public: + uint32_t Pack(const std::vector &cookies, uint32_t& version, uint32_t& checksum, string& payload) const + { + uint32_t rc = Core::ERROR_GENERAL; + + payload.clear(); + + const string serialized = serialize(cookies); + + if (serialized.size() == 0) { + rc = Core::ERROR_NONE; + } + else { + const std::vector compressed = compress(serialized); + ASSERT(compressed.size() != 0); + + if (compressed.size() == 0) { + TRACE_GLOBAL(Trace::Error, (_T("Compression failed"))); + } + else { + uint32_t encryptedSize = (compressed.size() + 32 /* some headroom for padding and IV */); + uint8_t* const encrypted = new (std::nothrow) uint8_t[encryptedSize]; + ASSERT(encrypted != nullptr); + + encryptedSize = Cipher(true, compressed.size(), compressed.data(), encryptedSize, encrypted); + + if (encryptedSize == 0) { + TRACE_GLOBAL(Trace::Error,(_T("Encryption failed"))); + } + else { + Core::ToString(encrypted, encryptedSize, true, payload); + ASSERT(payload.empty() == false); + + if (payload.empty() == true) { + TRACE_GLOBAL(Trace::Error,(_T("Encoding failed"))); + } + else { + version = PAYLOAD_VERSION; + checksum = crc_checksum(serialized); + rc = Core::ERROR_NONE; + } + } + + delete[] encrypted; + } } return rc; } - uint32_t Unpack(const uint32_t version, const uint32_t checksum, const string& payload, std::vector& cookies) + uint32_t Unpack(const uint32_t version, const uint32_t checksum, const string& payload, std::vector& cookies) const { uint32_t rc = Core::ERROR_GENERAL; - std::vector decrypted; - rc = _cookieJarCrypto.Decrypt(fromBase64(payload), version, decrypted); + cookies.clear(); - if (rc != Core::ERROR_NONE) - { - TRACE_GLOBAL(Trace::Error,(_T("Decryption failed, rc = %u"), rc)); + if (payload.size() == 0) { + rc = Core::ERROR_NONE; } - else - { - std::string serialized; - uint32_t actualChecksum; - - serialized = uncompress(decrypted); - actualChecksum = crc_checksum(serialized); - if (actualChecksum != checksum) { - rc = Core::ERROR_GENERAL; - TRACE_GLOBAL(Trace::Error,(_T("Checksum does not match: actual=%d expected=%d"), actualChecksum, checksum)); - } else { - deserialize(serialized, cookies); + else if (version != PAYLOAD_VERSION) { + TRACE_GLOBAL(Trace::Error,(_T("Unsupported payload version (%d)"), version)); + } + else { + uint32_t unencodedSize = ((payload.size() * 3) / 4); // base64 + uint8_t* const unencoded = new (std::nothrow) uint8_t[unencodedSize]; + ASSERT(unencoded != nullptr); + + const uint32_t decoded = Core::FromString(payload, unencoded, unencodedSize); + + if ((unencodedSize == 0) || (decoded != payload.size())) { + TRACE_GLOBAL(Trace::Error,(_T("Decoding failed"))); } + else { + std::vector decrypted(unencodedSize + 32); + + const uint32_t decryptedSize = Cipher(false, unencodedSize, unencoded, decrypted.size(), decrypted.data()); + decrypted.resize(decryptedSize); // does not realloc! + + if (decryptedSize == 0) { + TRACE_GLOBAL(Trace::Error,(_T("Decryption failed"))); + } + else { + const string serialized = uncompress(decrypted); + + if (serialized.size() == 0) { + TRACE_GLOBAL(Trace::Error,(_T("Decompression failed"))); + } + else { + const uint32_t actualChecksum = crc_checksum(serialized); + + if (actualChecksum != checksum) { + TRACE_GLOBAL(Trace::Error,(_T("Checksum does not match: actual=%04x expected=%04x"), actualChecksum, checksum)); + } + else { + deserialize(serialized, cookies); + rc = Core::ERROR_NONE; + } + } + } + } + + delete[] unencoded; } return rc; } + +private: + uint32_t Cipher(const bool encrypt, const uint32_t inputSize, const uint8_t input[], const uint32_t outputSize, uint8_t output[]) const + { + uint32_t rc = 0; + + ASSERT(inputSize != 0); + ASSERT(input != nullptr); + ASSERT(outputSize != 0); + ASSERT(output != nullptr); + + _lock.Lock(); + + uint32_t id = 0; + Exchange::IVault* vault = nullptr; + Exchange::ICryptography* crypto = nullptr; + + if (_connector.empty() == false) { + // Don't allow local cryptography instance. + crypto = Exchange::ICryptography::Instance(_connector); + } + + if (crypto == nullptr) { + TRACE_GLOBAL(Trace::Error, (_T("Cryptography is not available"))); + } + else { + Exchange::IDeviceObjects* const objects = Exchange::IDeviceObjects::Instance(); + if (objects != nullptr) { + id = objects->Id(_key, vault); + + if (vault == nullptr) { + ASSERT(id == 0); + TRACE_GLOBAL(Trace::Error, (_T("CookieJar encryption key '%s' not provisioned"), _key.c_str())); + } + + objects->Release(); + } + else { + TRACE_GLOBAL(Trace::Error, (_T("Svalbard not available"))); + } + } + + if (vault != nullptr) { + ASSERT(id != 0); + + Exchange::ICipher* const cipher = vault->AES(Exchange::aesmode::CBC, id); + + if (cipher == nullptr) { + TRACE_GLOBAL(Trace::Error, (_T("Cipher failed (unsupported method)"))); + } + else { + static constexpr uint16_t IV_SIZE = 16; + + const uint8_t* iv = nullptr; + const uint8_t* inputBuffer = nullptr; + uint32_t inputBufferSize = 0; + uint8_t* outputBuffer = nullptr; + uint32_t outputBufferSize = 0; + + if (encrypt == true) { + if (outputSize >= IV_SIZE) { + Exchange::IRandom* const random = crypto->Random(); + ASSERT(random != nullptr); + + if (random != nullptr) { + uint8_t* newIV = output; + + if (random->Generate(IV_SIZE, newIV) == IV_SIZE) { + iv = newIV; + inputBuffer = input; + inputBufferSize = inputSize; + outputBuffer = (output + IV_SIZE); + outputBufferSize = (outputSize - IV_SIZE); + } + + random->Release(); + } + } + } + else if (inputSize >= IV_SIZE) { + iv = input; + inputBuffer = (input + IV_SIZE); + inputBufferSize = (inputSize - IV_SIZE); + outputBuffer = output; + outputBufferSize = outputSize; + } + + if (inputBufferSize == 0) { + TRACE_GLOBAL(Trace::Error, (_T("Cipher failed (invalid input)"))); + } + else { + ASSERT(iv != nullptr); + ASSERT(inputBuffer != nullptr); + ASSERT(outputBuffer != nullptr); + + if (encrypt == true) { + rc = cipher->Encrypt(IV_SIZE, iv, inputBufferSize, inputBuffer, outputBufferSize, outputBuffer); + + if (rc != 0) { + rc += IV_SIZE; + } + } + else { + rc = cipher->Decrypt(IV_SIZE, iv, inputBufferSize, inputBuffer, outputBufferSize, outputBuffer); + } + + if (rc == 0) { + TRACE_GLOBAL(Trace::Error, (_T("Cipher failed"))); + } + } + + cipher->Release(); + } + + vault->Release(); + } + + if (crypto != nullptr) { + crypto->Release(); + } + + _lock.Unlock(); + + return (rc); + } + +private: + mutable Core::CriticalSection _lock; + string _connector; + string _key; }; CookieJar::CookieJar() - : _priv(new CookieJarPrivate) + : _priv() { + _priv = std::unique_ptr(new CookieJar::CookieJarPrivate()); } -CookieJar::~CookieJar() = default; +CookieJar::~CookieJar() +{ +} + +uint32_t CookieJar::Configure(const string& connector, const string& key) +{ + return (_priv->Configure(connector, key)); +} uint32_t CookieJar::Pack(uint32_t& version, uint32_t& checksum, string& payload) const { @@ -270,4 +484,4 @@ std::vector CookieJar::GetCookies() const } } // namespace Plugin -} // namespace WPEFramework +} diff --git a/WebKitBrowser/CookieJar.h b/WebKitBrowser/CookieJar.h index 9f875d56..fc08a9b7 100644 --- a/WebKitBrowser/CookieJar.h +++ b/WebKitBrowser/CookieJar.h @@ -25,6 +25,7 @@ #include #include + namespace WPEFramework { namespace Plugin { @@ -34,6 +35,8 @@ class CookieJar CookieJar(); ~CookieJar(); + uint32_t Configure(const string& connector = _T(""), const string& key = _T("")); + bool IsStale() const { return _refreshed.GetState() == false; } void MarkAsStale() { _refreshed.SetState( false ); } bool WaitForRefresh(int timeout_ms) const { return _refreshed.WaitState(true, timeout_ms); } diff --git a/WebKitBrowser/WebKitBrowser.cpp b/WebKitBrowser/WebKitBrowser.cpp index 14c5699d..ebd459e6 100644 --- a/WebKitBrowser/WebKitBrowser.cpp +++ b/WebKitBrowser/WebKitBrowser.cpp @@ -93,6 +93,20 @@ namespace Plugin { } else { message = _T("WebKitBrowser Application interface could not be obtained"); } + + PluginHost::ISubSystem::INotification* subsystemNotify = _browser->QueryInterface(); + + if (subsystemNotify != nullptr) { + PluginHost::ISubSystem* subsystems = _service->SubSystems(); + + if (subsystems != nullptr) { + subsystems->Register(subsystemNotify); + subsystems->Release(); + } + + subsystemNotify->Release(); + } + stateControl->Release(); } } diff --git a/WebKitBrowser/WebKitImplementation.cpp b/WebKitBrowser/WebKitImplementation.cpp index d383b5e1..7cf876c0 100644 --- a/WebKitBrowser/WebKitImplementation.cpp +++ b/WebKitBrowser/WebKitImplementation.cpp @@ -26,6 +26,10 @@ #include "Module.h" +#include +#include + + #ifdef WEBKIT_GLIB_API #include #include "Tags.h" @@ -67,7 +71,6 @@ WK_EXPORT void WKPreferencesSetPageCacheEnabled(WKPreferencesRef preferences, bo #include #include "HTML5Notification.h" -#include "WebKitBrowser.h" #if defined(ENABLE_CLOUD_COOKIE_JAR) #include "CookieJar.h" @@ -369,15 +372,28 @@ static GSourceFuncs _handlerIntervention = } } + static bool EnvironmentOverride(const bool configFlag) + { + bool result = configFlag; + + if (result == false) { + string value; + Core::SystemInfo::GetEnvironment(_T("WPE_ENVIRONMENT_OVERRIDE"), value); + result = (value == "1"); + } + return (result); + } + class WebKitImplementation : public Core::Thread, public Exchange::IBrowser, public Exchange::IWebBrowser, public Exchange::IApplication, public Exchange::IBrowserScripting, - #if defined(ENABLE_CLOUD_COOKIE_JAR) +#if defined(ENABLE_CLOUD_COOKIE_JAR) public Exchange::IBrowserCookieJar, - #endif - public PluginHost::IStateControl { +#endif + public PluginHost::IStateControl, + public PluginHost::ISubSystem::INotification { public: class BundleConfig : public Core::JSON::Container { private: @@ -566,7 +582,11 @@ static GSourceFuncs _handlerIntervention = , Whitelist() , PageGroup(_T("WPEPageGroup")) , CookieStorage() +#if defined(ENABLE_CLOUD_COOKIE_JAR) , CloudCookieJarEnabled(false) + , CloudCookieJarKeyName(_T("WebKitBrowser.cookiejar")) + , CryptoConnector(_T("/tmp/svalbard")) +#endif , LocalStorage() , LocalStorageEnabled(false) , LocalStorageSize() @@ -630,7 +650,11 @@ static GSourceFuncs _handlerIntervention = Add(_T("whitelist"), &Whitelist); Add(_T("pagegroup"), &PageGroup); Add(_T("cookiestorage"), &CookieStorage); +#if defined(ENABLE_CLOUD_COOKIE_JAR) Add(_T("cloudcookiejarenabled"), &CloudCookieJarEnabled); + Add(_T("cloudcookiejarkeyname"), &CloudCookieJarKeyName); + Add(_T("cryptoconnector"), &CryptoConnector); +#endif Add(_T("localstorage"), &LocalStorage); Add(_T("localstorageenabled"), &LocalStorageEnabled); Add(_T("localstoragesize"), &LocalStorageSize); @@ -701,7 +725,11 @@ static GSourceFuncs _handlerIntervention = Core::JSON::String Whitelist; Core::JSON::String PageGroup; Core::JSON::String CookieStorage; +#if defined(ENABLE_CLOUD_COOKIE_JAR) Core::JSON::Boolean CloudCookieJarEnabled; + Core::JSON::String CloudCookieJarKeyName; + Core::JSON::String CryptoConnector; +#endif Core::JSON::String LocalStorage; Core::JSON::Boolean LocalStorageEnabled; Core::JSON::DecUInt16 LocalStorageSize; @@ -928,9 +956,37 @@ static GSourceFuncs _handlerIntervention = TRACE(Trace::Information, (_T("Bailed out before the end of the WPE main app was reached. %d"), 6000)); } + if (_service != nullptr) { + _service->Release(); + _service = nullptr; + } + implementation = nullptr; } + private: + void SubSystemsChanged() + { + ASSERT(_service != nullptr); + +#if defined(ENABLE_CLOUD_COOKIE_JAR) + PluginHost::ISubSystem* subsystems = _service->SubSystems(); + + if (subsystems != nullptr) { + if (subsystems->IsActive(PluginHost::ISubSystem::CRYPTOGRAPHY) == true) { + if (_cookieJar.Configure(_config.CryptoConnector.Value(), _config.CloudCookieJarKeyName.Value()) != Core::ERROR_NONE) { + TRACE(Trace::Error, (_T("Failed to configure cloud CookieJar handler"))); + } + } + else { + _cookieJar.Configure(); + } + + subsystems->Release(); + } +#endif // defined(ENABLE_CLOUD_COOKIE_JAR) + } + public: uint32_t HeaderList(string& headerlist) const override { @@ -1927,7 +1983,6 @@ static GSourceFuncs _handlerIntervention = uint32_t Identifier(string& id) const override { - PluginHost::ISubSystem* subSystem = _service->SubSystems(); if (subSystem) { const PluginHost::ISubSystem::IIdentifier* identifier(subSystem->Get()); @@ -2199,12 +2254,23 @@ static GSourceFuncs _handlerIntervention = _httpStatusCode = code; } + // PluginHost::ISubSystem::INotification overrides + void Updated() override + { + SubSystemsChanged(); + } + + // Exchange::IConfiguration overrides uint32_t Configure(PluginHost::IShell* service) override { + ASSERT(service != nullptr); + #ifndef WEBKIT_GLIB_API _consoleLogPrefix = service->Callsign(); #endif + _service = service; + _service->AddRef(); _dataPath = service->DataPath(); @@ -2226,7 +2292,7 @@ static GSourceFuncs _handlerIntervention = } #endif - bool environmentOverride(WebKitBrowser::EnvironmentOverride(_config.EnvironmentOverride.Value())); + const bool environmentOverride = EnvironmentOverride(_config.EnvironmentOverride.Value()); if ((environmentOverride == false) || (Core::SystemInfo::GetEnvironment(_T("WPE_WEBKIT_URL"), _URL) == false)) { _URL = _config.URL.Value(); @@ -2514,6 +2580,7 @@ static GSourceFuncs _handlerIntervention = INTERFACE_ENTRY (Exchange::IBrowserCookieJar) #endif INTERFACE_ENTRY(PluginHost::IStateControl) + INTERFACE_ENTRY(PluginHost::ISubSystem::INotification) END_INTERFACE_MAP private: