diff --git a/build-aux/com.obsproject.Studio.json b/build-aux/com.obsproject.Studio.json index 56b0f5d239ec1b..ecadc7366e3b79 100644 --- a/build-aux/com.obsproject.Studio.json +++ b/build-aux/com.obsproject.Studio.json @@ -57,6 +57,7 @@ "modules/40-usrsctp.json", "modules/50-jansson.json", "modules/50-libdatachannel.json", + "modules/50-libsecret.json", "modules/50-ntv2.json", "modules/50-pipewire.json", "modules/50-swig.json", diff --git a/build-aux/modules/50-libsecret.json b/build-aux/modules/50-libsecret.json new file mode 100644 index 00000000000000..22263b46c067df --- /dev/null +++ b/build-aux/modules/50-libsecret.json @@ -0,0 +1,23 @@ +{ + "name": "libsecret", + "buildsystem": "meson", + "config-opts": [ + "-Dmanpage=false", + "-Dvapi=false", + "-Dgtk_doc=false", + "-Dintrospection=false" + ], + "cleanup": [ + "/bin", + "/include", + "/lib/pkgconfig", + "/share/man" + ], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz", + "sha256": "3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d" + } + ] +} diff --git a/cmake/Modules/FindLibsecret.cmake b/cmake/Modules/FindLibsecret.cmake new file mode 100644 index 00000000000000..2a3bb8d356f136 --- /dev/null +++ b/cmake/Modules/FindLibsecret.cmake @@ -0,0 +1,111 @@ +#[=======================================================================[.rst +FindLibsecret +------------- + +FindModule for libsecret and the associated library + +Imported Targets +^^^^^^^^^^^^^^^^ + +.. versionadded:: 2.0 + +This module defines the :prop_tgt:`IMPORTED` target ``Libsecret::Libsecret``. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module sets the following variables: + +``Libsecret_FOUND`` + True, if the library was found. +``Libsecret_VERSION`` + Detected version of found Libsecret library. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Libsecret_INCLUDE_DIR`` + Directory containing ``secret.h``. + +#]=======================================================================] + +include(FindPackageHandleStandardArgs) + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_search_module(PC_Libsecret QUIET libsecret-1) +endif() + +# Libsecret_set_soname: Set SONAME on imported library target +macro(Libsecret_set_soname) + if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") + execute_process( + COMMAND sh -c "${CMAKE_OBJDUMP} -p '${Libsecret_LIBRARY}' | grep SONAME" + OUTPUT_VARIABLE _output + RESULT_VARIABLE _result) + + if(_result EQUAL 0) + string(REGEX REPLACE "[ \t]+SONAME[ \t]+([^ \t]+)" "\\1" _soname "${_output}") + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_SONAME "${_soname}") + unset(_soname) + endif() + endif() + unset(_output) + unset(_result) +endmacro() + +find_path( + Libsecret_INCLUDE_DIR + NAMES libsecret/secret.h + HINTS ${PC_Libsecret_INCLUDE_DIRS} + PATHS /usr/include /usr/local/include + PATH_SUFFIXES libsecret-1 + DOC "Libsecret include directory") + +find_library( + Libsecret_LIBRARY + NAMES secret secret-1 libsecret libsecret-1 + HINTS ${PC_Libsecret_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib + DOC "Libsecret location") + +if(PC_Libsecret_VERSION VERSION_GREATER 0) + set(Libsecret_VERSION ${PC_Libsecret_VERSION}) +elseif(EXISTS "${Libsecret_INCLUDE_DIR}/secret-version.h") + file(STRINGS "${Libsecret_INCLUDE_DIR}/secret-version.h" _version_string) + string(REGEX REPLACE "SECRET_[A-Z]+_VERSION \\(([0-9]+)\\)" "\\1\\.\\2\\.\\3" Libsecret_VERSION "${_version_string}") +else() + if(NOT Libsecret_FIND_QUIETLY) + message(AUTHOR_WARNING "Failed to find Libsecret version.") + endif() + set(Libsecret_VERSION 0.0.0) +endif() + +find_package_handle_standard_args( + Libsecret + REQUIRED_VARS Libsecret_INCLUDE_DIR Libsecret_LIBRARY + VERSION_VAR Libsecret_VERSION REASON_FAILURE_MESSAGE "Ensure libsecret-1 is installed on the system.") +mark_as_advanced(Libsecret_INCLUDE_DIR Libsecret_LIBRARY) + +if(Libsecret_FOUND) + if(NOT TARGET Libsecret::Libsecret) + if(IS_ABSOLUTE "${Libsecret_LIBRARY}") + add_library(Libsecret::Libsecret UNKNOWN IMPORTED) + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_LOCATION "${Libsecret_LIBRARY}") + else() + add_library(Libsecret::Libsecret INTERFACE IMPORTED) + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_LIBNAME "${Libsecret_LIBRARY}") + endif() + + libsecret_set_soname() + set_target_properties(Libsecret::Libsecret PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${Libsecret_INCLUDE_DIR}") + endif() +endif() + +include(FeatureSummary) +set_package_properties( + Libsecret PROPERTIES + URL "https://gnome.pages.gitlab.gnome.org/libsecret/index.html" + DESCRIPTION "Secret Service D-Bus client library") diff --git a/cmake/finders/FindLibsecret.cmake b/cmake/finders/FindLibsecret.cmake new file mode 100644 index 00000000000000..2a3bb8d356f136 --- /dev/null +++ b/cmake/finders/FindLibsecret.cmake @@ -0,0 +1,111 @@ +#[=======================================================================[.rst +FindLibsecret +------------- + +FindModule for libsecret and the associated library + +Imported Targets +^^^^^^^^^^^^^^^^ + +.. versionadded:: 2.0 + +This module defines the :prop_tgt:`IMPORTED` target ``Libsecret::Libsecret``. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module sets the following variables: + +``Libsecret_FOUND`` + True, if the library was found. +``Libsecret_VERSION`` + Detected version of found Libsecret library. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Libsecret_INCLUDE_DIR`` + Directory containing ``secret.h``. + +#]=======================================================================] + +include(FindPackageHandleStandardArgs) + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_search_module(PC_Libsecret QUIET libsecret-1) +endif() + +# Libsecret_set_soname: Set SONAME on imported library target +macro(Libsecret_set_soname) + if(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") + execute_process( + COMMAND sh -c "${CMAKE_OBJDUMP} -p '${Libsecret_LIBRARY}' | grep SONAME" + OUTPUT_VARIABLE _output + RESULT_VARIABLE _result) + + if(_result EQUAL 0) + string(REGEX REPLACE "[ \t]+SONAME[ \t]+([^ \t]+)" "\\1" _soname "${_output}") + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_SONAME "${_soname}") + unset(_soname) + endif() + endif() + unset(_output) + unset(_result) +endmacro() + +find_path( + Libsecret_INCLUDE_DIR + NAMES libsecret/secret.h + HINTS ${PC_Libsecret_INCLUDE_DIRS} + PATHS /usr/include /usr/local/include + PATH_SUFFIXES libsecret-1 + DOC "Libsecret include directory") + +find_library( + Libsecret_LIBRARY + NAMES secret secret-1 libsecret libsecret-1 + HINTS ${PC_Libsecret_LIBRARY_DIRS} + PATHS /usr/lib /usr/local/lib + DOC "Libsecret location") + +if(PC_Libsecret_VERSION VERSION_GREATER 0) + set(Libsecret_VERSION ${PC_Libsecret_VERSION}) +elseif(EXISTS "${Libsecret_INCLUDE_DIR}/secret-version.h") + file(STRINGS "${Libsecret_INCLUDE_DIR}/secret-version.h" _version_string) + string(REGEX REPLACE "SECRET_[A-Z]+_VERSION \\(([0-9]+)\\)" "\\1\\.\\2\\.\\3" Libsecret_VERSION "${_version_string}") +else() + if(NOT Libsecret_FIND_QUIETLY) + message(AUTHOR_WARNING "Failed to find Libsecret version.") + endif() + set(Libsecret_VERSION 0.0.0) +endif() + +find_package_handle_standard_args( + Libsecret + REQUIRED_VARS Libsecret_INCLUDE_DIR Libsecret_LIBRARY + VERSION_VAR Libsecret_VERSION REASON_FAILURE_MESSAGE "Ensure libsecret-1 is installed on the system.") +mark_as_advanced(Libsecret_INCLUDE_DIR Libsecret_LIBRARY) + +if(Libsecret_FOUND) + if(NOT TARGET Libsecret::Libsecret) + if(IS_ABSOLUTE "${Libsecret_LIBRARY}") + add_library(Libsecret::Libsecret UNKNOWN IMPORTED) + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_LOCATION "${Libsecret_LIBRARY}") + else() + add_library(Libsecret::Libsecret INTERFACE IMPORTED) + set_property(TARGET Libsecret::Libsecret PROPERTY IMPORTED_LIBNAME "${Libsecret_LIBRARY}") + endif() + + libsecret_set_soname() + set_target_properties(Libsecret::Libsecret PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${Libsecret_INCLUDE_DIR}") + endif() +endif() + +include(FeatureSummary) +set_package_properties( + Libsecret PROPERTIES + URL "https://gnome.pages.gitlab.gnome.org/libsecret/index.html" + DESCRIPTION "Secret Service D-Bus client library") diff --git a/docs/sphinx/reference-libobs-util-platform.rst b/docs/sphinx/reference-libobs-util-platform.rst index 3584e282e3456c..a3d40752a3b901 100644 --- a/docs/sphinx/reference-libobs-util-platform.rst +++ b/docs/sphinx/reference-libobs-util-platform.rst @@ -505,3 +505,34 @@ Other Functions Must be freed with :c:func:`bfree()`. .. versionadded:: 29.1 + +--------------------- + +.. function:: bool *os_keychain_available(void) + + Indicates whether or not the keychain APIs are implemented on this platform. + + On Windows/macOS this will always return `true` and the keychain is guaranteed to be available. + On Linux it will return `true` if OBS is compiled with libsecret, but keychain operations may still fail if no Secret Service (e.g. kwaller or gnome-keyring) is available. + +--------------------- + +.. function:: bool os_keychain_save(const char *label, const char *key, const char *data) + + Saves the string `data` into the OS keychain as key `key` with user-visible name `label`. + + `label` should be a short descriptor of the kind of data being saved (e.g. "OBS Studio OAuth Credentials"), must not be translated, and must be identical when attempting to save/load/delete the same `key`. + +--------------------- + +.. function:: bool os_keychain_load(const char *label, const char *key, char **data) + + Attempt to read the string saved with key `key` in and with label `label` from the keychain. + + If successful, `data´ must be freed with :c:func:`bfree()`. + +--------------------- + +.. function:: bool os_keychain_delete(const char *label, const char *key) + + Deletes an item from the keychain. diff --git a/libobs/cmake/legacy.cmake b/libobs/cmake/legacy.cmake index e8458591c4f2f9..c923b6fe24c01e 100644 --- a/libobs/cmake/legacy.cmake +++ b/libobs/cmake/legacy.cmake @@ -316,7 +316,7 @@ if(OS_WINDOWS) target_compile_definitions(libobs PRIVATE UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS) set_source_files_properties(obs-win-crash-handler.c PROPERTIES COMPILE_DEFINITIONS OBS_VERSION="${OBS_VERSION_CANONICAL}") - target_link_libraries(libobs PRIVATE dxgi Avrt Dwmapi winmm Rpcrt4) + target_link_libraries(libobs PRIVATE dxgi Advapi32 Avrt Dwmapi winmm Rpcrt4) if(MSVC) target_link_libraries(libobs PUBLIC OBS::w32-pthreads) @@ -424,6 +424,14 @@ elseif(OS_POSIX) target_link_libraries(libobs PRIVATE GIO::GIO) target_sources(libobs PRIVATE util/platform-nix-dbus.c util/platform-nix-portal.c) + + find_package(Libsecret) + if(TARGET Libsecret::Libsecret) + obs_status(STATUS "-> libsecret found, enabling keychain API") + target_link_libraries(libobs PRIVATE Libsecret::Libsecret) + target_compile_definitions(libobs PRIVATE USE_LIBSECRET) + target_sources(libobs PRIVATE util/platform-nix-libsecret.c) + endif() endif() if(TARGET XCB::XINPUT) diff --git a/libobs/cmake/os-linux.cmake b/libobs/cmake/os-linux.cmake index 9aa825f4c1b175..f931d5b9a9e0ac 100644 --- a/libobs/cmake/os-linux.cmake +++ b/libobs/cmake/os-linux.cmake @@ -5,6 +5,7 @@ find_package(x11-xcb REQUIRED) find_package(xcb COMPONENTS xcb OPTIONAL_COMPONENTS xcb-xinput QUIET) # cmake-format: on find_package(gio) +find_package(Libsecret) target_sources( libobs @@ -43,6 +44,15 @@ endif() if(TARGET gio::gio) target_sources(libobs PRIVATE util/platform-nix-dbus.c util/platform-nix-portal.c) target_link_libraries(libobs PRIVATE gio::gio) + + if(TARGET Libsecret::Libsecret) + target_compile_definitions(libobs PRIVATE USE_LIBSECRET) + target_sources(libobs PRIVATE util/platform-nix-libsecret.c) + target_link_libraries(libobs PRIVATE Libsecret::Libsecret) + target_enable_feature(libobs "Libsecret Keychain API (Linux)") + else() + target_disable_feature(libobs "Libsecret Keychain API (Linux)") + endif() endif() if(ENABLE_WAYLAND) diff --git a/libobs/cmake/os-macos.cmake b/libobs/cmake/os-macos.cmake index e3da29797b27a3..f2283e6a421a99 100644 --- a/libobs/cmake/os-macos.cmake +++ b/libobs/cmake/os-macos.cmake @@ -6,7 +6,8 @@ target_link_libraries( "$" "$" "$" - "$") + "$" + "$") target_sources( libobs diff --git a/libobs/cmake/os-windows.cmake b/libobs/cmake/os-windows.cmake index 04500d1b04cf8c..ecf0e9b96ebc5d 100644 --- a/libobs/cmake/os-windows.cmake +++ b/libobs/cmake/os-windows.cmake @@ -46,6 +46,7 @@ set_source_files_properties(obs-win-crash-handler.c PROPERTIES COMPILE_DEFINITIO target_link_libraries( libobs PRIVATE Avrt + Advapi32 Dwmapi Dxgi winmm diff --git a/libobs/util/platform-cocoa.m b/libobs/util/platform-cocoa.m index f01a4202c9868b..cbec6c0328cf53 100644 --- a/libobs/util/platform-cocoa.m +++ b/libobs/util/platform-cocoa.m @@ -35,6 +35,7 @@ #include #import +#import #include "apple/cfstring-utils.h" @@ -505,3 +506,90 @@ bool cfstr_copy_dstr(CFStringRef cfstring, CFStringEncoding cfstring_encoding, s return (bool) success; } + +bool os_keychain_available(void) +{ + return true; +} + +bool os_keychain_save(const char *label, const char *key, const char *data) +{ + if (!label || !key || !data) + return false; + + NSData *nsData = [NSData dataWithBytesNoCopy:(void *) data length:strlen(data) freeWhenDone:NO]; + NSDictionary *insert = @{ + (__bridge id) kSecAttrAccessible: (__bridge id) kSecAttrAccessibleWhenUnlocked, + (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, + (__bridge id) kSecAttrService: @(label), + (__bridge id) kSecAttrAccount: @(key), + (__bridge id) kSecValueData: nsData, + }; + + OSStatus status = SecItemAdd((__bridge CFDictionaryRef) insert, nil); + /* macOS doesn't allow us to overwrite existing keychain items, so we need to do an update instead if already exists. */ + if (status == errSecDuplicateItem) { + NSDictionary *query = @{ + (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, + (__bridge id) kSecAttrService: @(label), + (__bridge id) kSecAttrAccount: @(key), + }; + status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) insert); + if (status != errSecSuccess) { + blog(LOG_ERROR, "Keychain item \"%s::%s\" could not be updated: %d", label, key, status); + } + } else if (status != errSecSuccess) { + blog(LOG_ERROR, "Keychain item \"%s::%s\" could not be saved: %d", label, key, status); + } + + return status == errSecSuccess; +} + +bool os_keychain_load(const char *label, const char *key, char **data) +{ + if (!label || !key || !data) + return false; + + NSDictionary *query = @{ + (__bridge id) kSecAttrAccessible: (__bridge id) kSecAttrAccessibleWhenUnlocked, + (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, + (__bridge id) kSecMatchLimit: (__bridge id) kSecMatchLimitOne, + (__bridge id) kSecAttrService: @(label), + (__bridge id) kSecAttrAccount: @(key), + (__bridge id) kSecReturnData: @YES, + }; + + /* The result will be a CFData holding the string we saved. */ + CFDataRef result = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef *) &result); + if (status != errSecSuccess || !result) { + blog(LOG_ERROR, "Keychain item \"%s::%s\" could not be read: %d", label, key, status); + return false; + } + + *data = bstrdup_n((const char *) CFDataGetBytePtr(result), CFDataGetLength(result)); + CFRelease(result); + + return true; +} + +bool os_keychain_delete(const char *label, const char *key) +{ + if (!label || !key) + return false; + + NSDictionary *query = @{ + (__bridge id) kSecAttrAccessible: (__bridge id) kSecAttrAccessibleWhenUnlocked, + (__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword, + (__bridge id) kSecAttrService: @(label), + (__bridge id) kSecAttrAccount: @(key), + }; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query); + if (status != errSecSuccess && status != errSecItemNotFound) { + blog(LOG_WARNING, "Keychain item \"%s::%s\" could not be deleted: %d", label, key, status); + return false; + } + + return true; +} diff --git a/libobs/util/platform-nix-libsecret.c b/libobs/util/platform-nix-libsecret.c new file mode 100644 index 00000000000000..9119c479e3798f --- /dev/null +++ b/libobs/util/platform-nix-libsecret.c @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 Dennis Sädtler + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include "platform.h" +#include "bmem.h" + +static const SecretSchema obs_schema = { + "com.obsproject.libobs.Secret", + SECRET_SCHEMA_NONE, + { + {"key", SECRET_SCHEMA_ATTRIBUTE_STRING}, + {"NULL", 0}, + }}; + +bool os_keychain_available(void) +{ + return true; +} + +bool os_keychain_save(const char *label, const char *key, const char *data) +{ + if (!label || !key || !data) + return false; + + GError *error = NULL; + secret_password_store_sync(&obs_schema, SECRET_COLLECTION_DEFAULT, + label, data, NULL, &error, "key", key, NULL); + if (error != NULL) { + blog(LOG_ERROR, + "Keychain item \"%s::%s\" could not be saved: %s", label, + key, error->message); + g_error_free(error); + return false; + } + + return true; +} + +bool os_keychain_load(const char *label, const char *key, char **data) +{ + if (!label || !key || !data) + return false; + + GError *error = NULL; + gchar *password = secret_password_lookup_sync(&obs_schema, NULL, &error, + "key", key, NULL); + + if (error != NULL) { + blog(LOG_ERROR, + "Keychain item \"%s::%s\" could not be read: %s", label, + key, error->message); + g_error_free(error); + return false; + } else if (password == NULL) { + return false; + } + + *data = bstrdup(password); + secret_password_free(password); + return true; +} + +bool os_keychain_delete(const char *label, const char *key) +{ + if (!label || !key) + return false; + + GError *error = NULL; + gboolean removed = secret_password_clear_sync(&obs_schema, NULL, &error, + "key", key, NULL); + + if (error != NULL) { + if (error->code == SECRET_ERROR_NO_SUCH_OBJECT) { + removed = true; + } else { + blog(LOG_ERROR, + "Keychain item \"%s::%s\" could not be deleted: %s", + label, key, error->message); + } + + g_error_free(error); + } + + return removed; +} diff --git a/libobs/util/platform-nix.c b/libobs/util/platform-nix.c index 59a3e4bfd97965..13648ae95e6cda 100644 --- a/libobs/util/platform-nix.c +++ b/libobs/util/platform-nix.c @@ -1148,3 +1148,36 @@ char *os_generate_uuid(void) uuid_unparse_lower(uuid, out); return out; } + +#if !defined(__APPLE__) && !defined(USE_LIBSECRET) +bool os_keychain_available(void) +{ + return false; +} + +bool os_keychain_save(const char *label, const char *key, const char *data) +{ + /* Not implemented */ + UNUSED_PARAMETER(label); + UNUSED_PARAMETER(key); + UNUSED_PARAMETER(data); + return false; +} + +bool os_keychain_load(const char *label, const char *key, char **data) +{ + /* Not implemented */ + UNUSED_PARAMETER(label); + UNUSED_PARAMETER(key); + UNUSED_PARAMETER(data); + return false; +} + +bool os_keychain_delete(const char *label, const char *key) +{ + /* Not implemented */ + UNUSED_PARAMETER(label); + UNUSED_PARAMETER(key); + return false; +} +#endif diff --git a/libobs/util/platform-windows.c b/libobs/util/platform-windows.c index 3a6706c3635b74..0c00a928201ec0 100644 --- a/libobs/util/platform-windows.c +++ b/libobs/util/platform-windows.c @@ -22,6 +22,7 @@ #include #include #include +#include #include "base.h" #include "platform.h" @@ -1498,3 +1499,108 @@ char *os_generate_uuid(void) return uuid_str.array; } + +bool os_keychain_available(void) +{ + return true; +} + +bool os_keychain_save(const char *label, const char *key, const char *data) +{ + if (!label || !key || !data) + return false; + + struct dstr uuid_str = {0}; + dstr_printf(&uuid_str, "%s::%s", label, key); + + wchar_t *target_name = NULL; + os_utf8_to_wcs_ptr(uuid_str.array, 0, &target_name); + dstr_free(&uuid_str); + + if (!target_name) + return false; + + size_t size = strlen(data); + + CREDENTIAL cred = {0}; + cred.CredentialBlob = (LPBYTE)data; + cred.CredentialBlobSize = (DWORD)size; + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + cred.TargetName = target_name; + cred.Type = CRED_TYPE_GENERIC; + + bool success = CredWriteW(&cred, 0); + if (!success) { + blog(LOG_ERROR, + "Keychain item \"%s::%s\" could not be saved: %d", label, + key, GetLastError()); + } + + bfree(target_name); + + return success; +} + +bool os_keychain_load(const char *label, const char *key, char **data) +{ + if (!label || !key || !data) + return false; + + struct dstr uuid_str = {0}; + dstr_printf(&uuid_str, "%s::%s", label, key); + + wchar_t *target_name = NULL; + os_utf8_to_wcs_ptr(uuid_str.array, 0, &target_name); + dstr_free(&uuid_str); + + if (!target_name) + return false; + + PCREDENTIALW cred; + bool success = CredReadW(target_name, CRED_TYPE_GENERIC, 0, &cred); + if (success) { + *data = bstrdup_n((const char *)cred->CredentialBlob, + cred->CredentialBlobSize); + CredFree(cred); + } else { + blog(LOG_ERROR, + "Keychain item \"%s::%s\" could not be read: %d", label, + key, GetLastError()); + } + + bfree(target_name); + + return success; +} + +bool os_keychain_delete(const char *label, const char *key) +{ + if (!label || !key) + return false; + + struct dstr uuid_str = {0}; + dstr_printf(&uuid_str, "%s::%s", label, key); + + wchar_t *target_name = NULL; + os_utf8_to_wcs_ptr(uuid_str.array, 0, &target_name); + dstr_free(&uuid_str); + + if (!target_name) + return false; + + bool success = CredDeleteW(target_name, CRED_TYPE_GENERIC, 0); + if (!success) { + DWORD err = GetLastError(); + if (err == ERROR_NOT_FOUND) { + success = true; + } else { + blog(LOG_WARNING, + "Keychain item \"%s::%s\" could not be deleted: %d", + label, key, err); + } + } + + bfree(target_name); + + return success; +} diff --git a/libobs/util/platform.h b/libobs/util/platform.h index a3a2b74c4369af..40c99e8be885e7 100644 --- a/libobs/util/platform.h +++ b/libobs/util/platform.h @@ -205,6 +205,12 @@ EXPORT uint64_t os_get_proc_virtual_size(void); EXPORT char *os_generate_uuid(void); +EXPORT bool os_keychain_available(void); +EXPORT bool os_keychain_save(const char *label, const char *key, + const char *data); +EXPORT bool os_keychain_load(const char *label, const char *key, char **data); +EXPORT bool os_keychain_delete(const char *label, const char *key); + /* clang-format off */ #ifdef __APPLE__ # define ARCH_BITS 64