From 14d9da78c986e0defde61745723387525da9ab96 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Fri, 22 Mar 2024 16:38:30 -0700 Subject: [PATCH] Allow setting a security access group for the metadata realm keychain Access groups are shared storage for one or more apps on iOS (and other Apple platforms). Sharing a metadata Realm between apps requires placing the file in the access group storage and storing the encryption key in the access group's keychain. Including the bundle ID in the service name breaks sharing the key between apps, as different apps will have different bundle IDs. For everything but un-sandboxed macOS there wasn't actually any reason to include the bundle ID in the first place, as each app has its own keychain anyway. As such, this switches back to not including it. On macOS this continues to include the bundle ID when not using an access group, as otherwise different applications could conflict with each other. This means that sharing users between macOS applications will currently only work if an encryption key is explicitly set or if the applications have sandboxing enabled. Since this is slightly changing how keys are stored anyway, it also switches to using unique keys per server app ID rather than always using "metadata" as the account name. The keychain code was mostly multiprocess-safe, but there was one race condition when two apps generated a new key at once which is fixed. --- CHANGELOG.md | 2 + src/realm.h | 2 + src/realm/object-store/c_api/sync.cpp | 9 + .../impl/apple/keychain_helper.cpp | 223 ++++++++++------ .../impl/apple/keychain_helper.hpp | 17 +- .../object-store/sync/impl/sync_metadata.cpp | 26 +- .../object-store/sync/impl/sync_metadata.hpp | 6 +- src/realm/object-store/sync/sync_manager.cpp | 4 +- src/realm/object-store/sync/sync_manager.hpp | 5 +- src/realm/sync/network/network_ssl.cpp | 21 +- src/realm/util/cf_str.hpp | 35 ++- test/object-store/c_api/c_api.cpp | 4 + test/object-store/sync/metadata.cpp | 244 +++++++++++++++--- test/object-store/sync/sync_manager.cpp | 8 +- test/object-store/sync/user.cpp | 4 +- 15 files changed, 442 insertions(+), 168 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 202b17ba046..40cc62ef1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Enhancements * Introduce sync 'progress_estimate' parameter (value from 0.0 to 1.0) for existing sync 'ProgressNotifierCallback' api to report sync progress on current batch of upload/download until completion ([#7450](https://github.com/realm/realm-core/issues/7450)) +* Add `SyncClientConfig::security_access_group` which allows specifying the access group to use for the sync metadata Realm's encryption key. Setting this is required when sharing the metadata Realm between apps on Apple platforms ([#7552](https://github.com/realm/realm-core/pull/7552)). +* When connecting to multiple server apps, a unique encryption key is used for each of the metadata Realms rather than sharing one between them ([#7552](https://github.com/realm/realm-core/pull/7552)). ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) diff --git a/src/realm.h b/src/realm.h index 4cedcd7824c..4fc95d1e1ba 100644 --- a/src/realm.h +++ b/src/realm.h @@ -3660,6 +3660,8 @@ RLM_API void realm_sync_client_config_set_default_binding_thread_observer( realm_sync_client_config_t* config, realm_on_object_store_thread_callback_t on_thread_create, realm_on_object_store_thread_callback_t on_thread_destroy, realm_on_object_store_error_callback_t on_error, realm_userdata_t user_data, realm_free_userdata_func_t free_userdata); +RLM_API void realm_sync_client_config_set_security_access_group(realm_sync_client_config_t*, + const char*) RLM_API_NOEXCEPT; RLM_API realm_sync_config_t* realm_sync_config_new(const realm_user_t*, const char* partition_value) RLM_API_NOEXCEPT; RLM_API realm_sync_config_t* realm_flx_sync_config_new(const realm_user_t*) RLM_API_NOEXCEPT; diff --git a/src/realm/object-store/c_api/sync.cpp b/src/realm/object-store/c_api/sync.cpp index 8a34d87f66e..2f1e435d5cf 100644 --- a/src/realm/object-store/c_api/sync.cpp +++ b/src/realm/object-store/c_api/sync.cpp @@ -230,6 +230,15 @@ RLM_API void realm_sync_client_config_set_resumption_delay_backoff_multiplier(re config->timeouts.reconnect_backoff_info.resumption_delay_backoff_multiplier = multiplier; } +#if REALM_PLATFORM_APPLE +RLM_API void realm_sync_client_config_set_security_access_group(realm_sync_client_config_t* config, + const char* group) noexcept +{ + config->security_access_group = group; +} +#endif + + /// Register an app local callback handler for bindings interested in registering callbacks before/after /// the ObjectStore thread runs for this app. This only works for the default socket provider implementation. /// IMPORTANT: If a function is supplied that handles the exception, it must call abort() or cause the diff --git a/src/realm/object-store/impl/apple/keychain_helper.cpp b/src/realm/object-store/impl/apple/keychain_helper.cpp index 5bc2532089f..d08832b77cf 100644 --- a/src/realm/object-store/impl/apple/keychain_helper.cpp +++ b/src/realm/object-store/impl/apple/keychain_helper.cpp @@ -18,8 +18,8 @@ #include -#include -#include +#include +#include #include @@ -28,33 +28,36 @@ using namespace realm; using util::adoptCF; using util::CFPtr; -using util::retainCF; +using util::string_view_to_cfstring; namespace { -std::runtime_error keychain_access_exception(int32_t error_code) +REALM_NORETURN +REALM_COLD +void keychain_access_exception(int32_t error_code) { - return std::runtime_error(util::format("Keychain returned unexpected status code: %1", error_code)); + if (auto message = adoptCF(SecCopyErrorMessageString(error_code, nullptr))) { + if (auto msg = CFStringGetCStringPtr(message.get(), kCFStringEncodingUTF8)) { + throw RuntimeError(ErrorCodes::RuntimeError, + util::format("Keychain returned unexpected status code: %1 (%2)", msg, error_code)); + } + auto length = CFStringGetMaximumSizeForEncoding(CFStringGetLength(message.get()), kCFStringEncodingUTF8) + 1; + auto buffer = std::make_unique(length); + if (CFStringGetCString(message.get(), buffer.get(), length, kCFStringEncodingUTF8)) { + throw RuntimeError( + ErrorCodes::RuntimeError, + util::format("Keychain returned unexpected status code: %1 (%2)", buffer.get(), error_code)); + } + } + throw RuntimeError(ErrorCodes::RuntimeError, + util::format("Keychain returned unexpected status code: %1", error_code)); } constexpr size_t key_size = 64; -const CFStringRef s_account = CFSTR("metadata"); -const CFStringRef s_legacy_service = CFSTR("io.realm.sync.keychain"); - -#if !TARGET_IPHONE_SIMULATOR -CFPtr convert_string(const std::string& string) -{ - auto result = adoptCF(CFStringCreateWithBytes(nullptr, reinterpret_cast(string.data()), - string.size(), kCFStringEncodingASCII, false)); - if (!result) { - throw std::bad_alloc(); - } - return result; -} -#endif +const CFStringRef s_legacy_account = CFSTR("metadata"); +const CFStringRef s_service = CFSTR("io.realm.sync.keychain"); -CFPtr build_search_dictionary(CFStringRef account, CFStringRef service, - __unused util::Optional group) +CFPtr build_search_dictionary(CFStringRef account, CFStringRef service, CFStringRef group) { auto d = adoptCF( CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); @@ -65,17 +68,20 @@ CFPtr build_search_dictionary(CFStringRef account, CFStr CFDictionaryAddValue(d.get(), kSecReturnData, kCFBooleanTrue); CFDictionaryAddValue(d.get(), kSecAttrAccount, account); CFDictionaryAddValue(d.get(), kSecAttrService, service); -#if !TARGET_IPHONE_SIMULATOR - if (group) - CFDictionaryAddValue(d.get(), kSecAttrAccessGroup, convert_string(*group).get()); -#endif + if (group) { + CFDictionaryAddValue(d.get(), kSecAttrAccessGroup, group); + if (__builtin_available(macOS 10.15, iOS 13.0, *)) { + CFDictionaryAddValue(d.get(), kSecUseDataProtectionKeychain, kCFBooleanTrue); + } + } return d; } /// Get the encryption key for a given service, returning true if it either exists or the keychain is not usable. -bool get_key(CFStringRef account, CFStringRef service, util::Optional>& result) +bool get_key(CFStringRef account, CFStringRef service, std::string_view group, + std::optional>& result, bool result_on_error = true) { - auto search_dictionary = build_search_dictionary(account, service, none); + auto search_dictionary = build_search_dictionary(account, service, string_view_to_cfstring(group).get()); CFDataRef retained_key_data; switch (OSStatus status = SecItemCopyMatching(search_dictionary.get(), (CFTypeRef*)&retained_key_data)) { case errSecSuccess: { @@ -96,109 +102,178 @@ bool get_key(CFStringRef account, CFStringRef service, util::Optional>& key, CFStringRef account, CFStringRef service) +bool set_key(std::optional>& key, CFStringRef account, CFStringRef service, std::string_view group) { + // key may be nullopt here if the keychain was inaccessible if (!key) - return; + return false; - auto search_dictionary = build_search_dictionary(account, service, none); + auto search_dictionary = build_search_dictionary(account, service, string_view_to_cfstring(group).get()); CFDictionaryAddValue(search_dictionary.get(), kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock); - auto key_data = adoptCF(CFDataCreate(nullptr, reinterpret_cast(key->data()), key_size)); + auto key_data = adoptCF(CFDataCreateWithBytesNoCopy(nullptr, reinterpret_cast(key->data()), + key_size, kCFAllocatorNull)); if (!key_data) throw std::bad_alloc(); CFDictionaryAddValue(search_dictionary.get(), kSecValueData, key_data.get()); switch (OSStatus status = SecItemAdd(search_dictionary.get(), nullptr)) { case errSecSuccess: - return; + return true; case errSecDuplicateItem: - // A keychain item already exists but we didn't fine it in get_key(), - // meaning that we didn't have permission to access it. + // A keychain item already exists but we didn't fine it in get_key(). + // Either someone else created it between when we last checked and + // now or we don't have permission to read it. Try to reread the key + // and discard the one we just created in case it's the former + if (get_key(account, service, group, key, false)) + return true; case errSecMissingEntitlement: case errSecUserCanceled: case errSecInteractionNotAllowed: case errSecInvalidKeychain: // We were unable to save the key for "expected" reasons, so proceed unencrypted - key = none; - return; + return false; default: // Unexpected keychain failure happened - throw keychain_access_exception(status); + keychain_access_exception(status); } } -void delete_key(CFStringRef account, CFStringRef service) +void delete_key(CFStringRef account, CFStringRef service, CFStringRef group) { - auto search_dictionary = build_search_dictionary(account, service, none); + auto search_dictionary = build_search_dictionary(account, service, group); auto status = SecItemDelete(search_dictionary.get()); REALM_ASSERT(status == errSecSuccess || status == errSecItemNotFound); } -CFPtr get_service_name(bool& have_bundle_id) +CFPtr bundle_service() { - CFPtr service; if (CFStringRef bundle_id = CFBundleGetIdentifier(CFBundleGetMainBundle())) { - service = adoptCF(CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id)); - have_bundle_id = true; - } - else { - service = retainCF(s_legacy_service); - have_bundle_id = false; + return adoptCF(CFStringCreateWithFormat(nullptr, nullptr, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id)); } - return service; + return CFPtr{}; } } // anonymous namespace namespace realm::keychain { -util::Optional> get_existing_metadata_realm_key() +std::optional> get_existing_metadata_realm_key(std::string_view app_id, + std::string_view access_group) { - bool have_bundle_id = false; - CFPtr service = get_service_name(have_bundle_id); + auto cf_app_id = string_view_to_cfstring(app_id); + std::optional> key; - // Try retrieving the existing key. - util::Optional> key; - if (get_key(s_account, service.get(), key)) { + // If we have a security access groups then keys are stored the same way + // everywhere and we don't have any legacy storage methods to handle, so + // we just either have a key or we don't. + if (access_group.size()) { + get_key(cf_app_id.get(), s_service, access_group, key); return key; } - if (have_bundle_id) { - // See if there's a key stored using the legacy shared keychain item. - if (get_key(s_account, s_legacy_service, key)) { - // If so, copy it to the per-app keychain item before returning it. - set_key(key, s_account, service.get()); + // When we don't have an access group we check a whole bunch of things because + // there's been a variety of ways that we've stored metadata keys over the years. + // If we find a key stored in a non-preferred way we copy it to the preferred + // location before returning it. + // + // The original location was (account: "metadata", service: "io.realm.sync.keychain"). + // For processes with a bundle ID, we then switched to (account: "metadata", + // service: "$bundleId - Realm Sync Metadata Key") + // The current preferred location on non-macOS (account: appId, service: "io.realm.sync.keychain"), + // and on macOS is (account: appId, service: "$bundleId - Realm Sync Metadata Key"). + // + // On everything but macOS the keychain is scoped to the app, so there's no + // need to include the bundle ID. On macOS it's user-wide, and we want each + // application using Realm to have separate state. Using multiple server apps + // in one client is unusual, but when it's done we want each metadata realm to + // have a separate key. + +#if TARGET_OS_OSX + if (auto service = bundle_service()) { + if (get_key(cf_app_id.get(), service.get(), {}, key)) + return key; + if (get_key(s_legacy_account, service.get(), {}, key)) { + set_key(key, cf_app_id.get(), service.get(), {}); + return key; + } + if (get_key(s_legacy_account, s_service, {}, key)) { + set_key(key, cf_app_id.get(), service.get(), {}); + return key; + } + } + else { + if (get_key(cf_app_id.get(), s_service, {}, key)) + return key; + if (get_key(s_legacy_account, s_service, {}, key)) { + set_key(key, cf_app_id.get(), s_service, {}); + return key; + } + } +#else + if (get_key(cf_app_id, s_service, {}, key)) + return key; + if (auto service = bundle_service()) { + if (get_key(cf_app_id, service, {}, key)) { + set_key(key, cf_app_id, s_service, {}); return key; } } - return util::none; + if (get_key(s_legacy_account, s_service, {}, key)) { + set_key(key, cf_app_id, s_service, {}); + return key; + } +#endif + + return key; } -util::Optional> create_new_metadata_realm_key() +std::optional> create_new_metadata_realm_key(std::string_view app_id, std::string_view access_group) { - bool have_bundle_id = false; - CFPtr service = get_service_name(have_bundle_id); - - util::Optional> key; + auto cf_app_id = string_view_to_cfstring(app_id); + std::optional> key; key.emplace(key_size); arc4random_buf(key->data(), key_size); - set_key(key, s_account, service.get()); + + // See above for why macOS is different +#if TARGET_OS_OSX + if (!access_group.size()) { + if (auto service = bundle_service()) { + if (!set_key(key, cf_app_id.get(), service.get(), {})) + key.reset(); + return key; + } + } +#endif + + // If we're unable to save the newly created key, clear it and proceed unencrypted + if (!set_key(key, cf_app_id.get(), s_service, access_group)) + key.reset(); return key; } -void delete_metadata_realm_encryption_key() +void delete_metadata_realm_encryption_key(std::string_view app_id, std::string_view access_group) { - delete_key(s_account, s_legacy_service); - if (CFStringRef bundle_id = CFBundleGetIdentifier(CFBundleGetMainBundle())) { - auto service = - adoptCF(CFStringCreateWithFormat(NULL, NULL, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id)); - delete_key(s_account, service.get()); + auto cf_app_id = string_view_to_cfstring(app_id); + if (access_group.size()) { + delete_key(cf_app_id.get(), s_service, string_view_to_cfstring(access_group).get()); + return; + } + + delete_key(cf_app_id.get(), s_service, {}); + delete_key(s_legacy_account, s_service, {}); + if (auto service = bundle_service()) { + delete_key(cf_app_id.get(), service.get(), {}); + delete_key(s_legacy_account, service.get(), {}); } } diff --git a/src/realm/object-store/impl/apple/keychain_helper.hpp b/src/realm/object-store/impl/apple/keychain_helper.hpp index cb6767d2cf9..064cd7e7d87 100644 --- a/src/realm/object-store/impl/apple/keychain_helper.hpp +++ b/src/realm/object-store/impl/apple/keychain_helper.hpp @@ -23,28 +23,25 @@ #if REALM_PLATFORM_APPLE -#include +#include +#include #include namespace realm::keychain { // Get the stored encryption key for the metadata realm if one exists. -util::Optional> get_existing_metadata_realm_key(); +std::optional> get_existing_metadata_realm_key(std::string_view app_id, + std::string_view access_group); // Create a new encryption key and store it in the keychain. Returns none if // the key could not be stored. -util::Optional> create_new_metadata_realm_key(); +std::optional> create_new_metadata_realm_key(std::string_view app_id, + std::string_view access_group); // Delete the encryption key for the metadata realm from the keychain. -void delete_metadata_realm_encryption_key(); +void delete_metadata_realm_encryption_key(std::string_view app_id, std::string_view access_group); } // namespace realm::keychain -#else // REALM_PLATFORM_APPLE - -namespace realm::keychain { -inline void delete_metadata_realm_encryption_key() {} -} // namespace realm::keychain - #endif // REALM_PLATFORM_APPLE #endif // REALM_OS_KEYCHAIN_HELPER_HPP diff --git a/src/realm/object-store/sync/impl/sync_metadata.cpp b/src/realm/object-store/sync/impl/sync_metadata.cpp index af75fc0a55b..fcc0cd63785 100644 --- a/src/realm/object-store/sync/impl/sync_metadata.cpp +++ b/src/realm/object-store/sync/impl/sync_metadata.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #if REALM_PLATFORM_APPLE @@ -204,22 +205,19 @@ void migrate_to_v8(Realm&, Realm& realm) // MARK: - Sync metadata manager -SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key) +SyncMetadataManager::SyncMetadataManager(const std::string& path, const SyncClientConfig& config, + std::string_view app_id) { constexpr uint64_t SCHEMA_VERSION = 7; - if (!REALM_PLATFORM_APPLE && should_encrypt && !encryption_key) - throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); - m_metadata_config.automatic_change_notifications = false; m_metadata_config.path = path; m_metadata_config.schema = make_schema(); m_metadata_config.schema_version = SCHEMA_VERSION; m_metadata_config.schema_mode = SchemaMode::Automatic; m_metadata_config.scheduler = util::Scheduler::make_dummy(); - if (encryption_key) - m_metadata_config.encryption_key = std::move(*encryption_key); + if (config.metadata_mode == SyncClientConfig::MetadataMode::Encryption && config.custom_encryption_key) + m_metadata_config.encryption_key = *config.custom_encryption_key; m_metadata_config.automatically_handle_backlinks_in_migrations = true; m_metadata_config.migration_function = [](std::shared_ptr old_realm, std::shared_ptr realm, Schema&) { @@ -232,7 +230,7 @@ SyncMetadataManager::SyncMetadataManager(std::string path, bool should_encrypt, } }; - auto realm = open_realm(should_encrypt, encryption_key != none); + auto realm = open_realm(config, app_id); // Get data about the (hardcoded) schemas auto object_schema = realm->schema().find(c_sync_userMetadata); @@ -524,9 +522,13 @@ std::shared_ptr SyncMetadataManager::try_get_realm() const } } -std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool caller_supplied_key) +std::shared_ptr SyncMetadataManager::open_realm(const SyncClientConfig& config, std::string_view app_id) { - if (caller_supplied_key || !should_encrypt || !REALM_PLATFORM_APPLE) { + bool should_encrypt = config.metadata_mode == SyncClientConfig::MetadataMode::Encryption; + if (!REALM_PLATFORM_APPLE && should_encrypt && !config.custom_encryption_key) + throw InvalidArgument("Metadata Realm encryption was specified, but no encryption key was provided."); + + if (config.custom_encryption_key || !should_encrypt || !REALM_PLATFORM_APPLE) { m_metadata_config.clear_on_invalid_file = true; return get_realm(); } @@ -539,7 +541,7 @@ std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool // First try to open the Realm with a key already stored in the keychain. // This works for both the case where everything is sensible and valid and // when we have a key but no metadata Realm. - auto key = keychain::get_existing_metadata_realm_key(); + auto key = keychain::get_existing_metadata_realm_key(app_id, config.security_access_group); if (key) { m_metadata_config.encryption_key = *key; if (auto realm = try_get_realm()) @@ -563,7 +565,7 @@ std::shared_ptr SyncMetadataManager::open_realm(bool should_encrypt, bool // try to create and store a new one. This might fail, in which case we // just create an unencrypted Realm file. if (!key) - key = keychain::create_new_metadata_realm_key(); + key = keychain::create_new_metadata_realm_key(app_id, config.security_access_group); if (key) m_metadata_config.encryption_key = std::move(*key); return get_realm(); diff --git a/src/realm/object-store/sync/impl/sync_metadata.hpp b/src/realm/object-store/sync/impl/sync_metadata.hpp index 4987c9e809f..baf5d71a41a 100644 --- a/src/realm/object-store/sync/impl/sync_metadata.hpp +++ b/src/realm/object-store/sync/impl/sync_metadata.hpp @@ -30,6 +30,7 @@ namespace realm { class SyncFileManager; class SyncMetadataManager; +struct SyncClientConfig; // A facade for a metadata Realm object representing a sync user. class SyncUserMetadata { @@ -220,8 +221,7 @@ class SyncMetadataManager { /// If the platform supports it, setting `should_encrypt` to `true` and not specifying an encryption key will make /// the object store handle generating and persisting an encryption key for the metadata database. Otherwise, an /// exception will be thrown. - SyncMetadataManager(std::string path, bool should_encrypt, - util::Optional> encryption_key = none); + SyncMetadataManager(const std::string& path, const SyncClientConfig& config, std::string_view app_id); private: SyncUserMetadataResults get_users(bool marked) const; @@ -231,7 +231,7 @@ class SyncMetadataManager { std::shared_ptr get_realm() const; std::shared_ptr try_get_realm() const; - std::shared_ptr open_realm(bool should_encrypt, bool caller_supplied_key); + std::shared_ptr open_realm(const SyncClientConfig& config, std::string_view app_id); bool run_file_action(SyncFileManager& file_manager, SyncFileActionMetadata& md) const; }; diff --git a/src/realm/object-store/sync/sync_manager.cpp b/src/realm/object-store/sync/sync_manager.cpp index bf86816606d..02e87babe36 100644 --- a/src/realm/object-store/sync/sync_manager.cpp +++ b/src/realm/object-store/sync/sync_manager.cpp @@ -65,9 +65,7 @@ SyncManager::SyncManager(Private, std::shared_ptr app, std::string syn return; } - bool encrypt = m_config.metadata_mode == MetadataMode::Encryption; - m_metadata_manager = std::make_unique(m_file_manager->metadata_path(), encrypt, - m_config.custom_encryption_key); + m_metadata_manager = std::make_unique(m_file_manager->metadata_path(), m_config, app_id); m_metadata_manager->perform_launch_actions(*m_file_manager); diff --git a/src/realm/object-store/sync/sync_manager.hpp b/src/realm/object-store/sync/sync_manager.hpp index ff683e9f4c4..c33e16c2ab9 100644 --- a/src/realm/object-store/sync/sync_manager.hpp +++ b/src/realm/object-store/sync/sync_manager.hpp @@ -75,7 +75,10 @@ struct SyncClientConfig { std::string base_file_path; MetadataMode metadata_mode = MetadataMode::Encryption; - util::Optional> custom_encryption_key; + std::optional> custom_encryption_key; +#if REALM_PLATFORM_APPLE + std::string security_access_group; +#endif using LoggerFactory = std::function(util::Logger::Level)>; LoggerFactory logger_factory; diff --git a/src/realm/sync/network/network_ssl.cpp b/src/realm/sync/network/network_ssl.cpp index ea4f5017f04..88b9b0ac19a 100644 --- a/src/realm/sync/network/network_ssl.cpp +++ b/src/realm/sync/network/network_ssl.cpp @@ -204,16 +204,13 @@ const char* SecureTransportErrorCategory::name() const noexcept std::string SecureTransportErrorCategory::message(int value) const { - std::string message = "Unknown error"; + const char* message = "Unknown error"; #if REALM_HAVE_SECURE_TRANSPORT -#if __has_builtin(__builtin_available) - if (__builtin_available(iOS 11.3, macOS 10.3, tvOS 11.3, watchOS 4.3, *)) { - auto status = OSStatus(value); - void* reserved = nullptr; - if (auto cf_message = adoptCF(SecCopyErrorMessageString(status, reserved))) - message = cfstring_to_std_string(cf_message.get()); - } -#endif // __has_builtin(__builtin_available) + auto status = OSStatus(value); + void* reserved = nullptr; + std::unique_ptr buffer; + if (auto cf_message = adoptCF(SecCopyErrorMessageString(status, reserved))) + message = cfstring_to_cstring(cf_message.get(), buffer); #endif // REALM_HAVE_SECURE_TRANSPORT return util::format("SecureTransport error: %1 (%2)", message, value); // Throws @@ -1174,9 +1171,11 @@ OSStatus Stream::verify_peer() noexcept CFErrorRef cfErrorRef; if (!SecTrustEvaluateWithError(peerTrust.get(), &cfErrorRef)) { auto cfError = util::adoptCF(cfErrorRef); - if (logger) { + if (logger && logger->would_log(Logger::Level::debug)) { auto errorStr = util::adoptCF(CFErrorCopyDescription(cfErrorRef)); - logger->debug("SSL peer verification failed: %1", cfstring_to_std_string(errorStr.get())); + std::unique_ptr buffer; + logger->debug("SSL peer verification failed: %1", + cfstring_to_cstring(errorStr.get(), buffer)); } return errSSLXCertChainInvalid; } diff --git a/src/realm/util/cf_str.hpp b/src/realm/util/cf_str.hpp index d9f53a6fddc..2064de622cc 100644 --- a/src/realm/util/cf_str.hpp +++ b/src/realm/util/cf_str.hpp @@ -23,31 +23,40 @@ #if REALM_PLATFORM_APPLE -#include +#include -namespace realm { -namespace util { +namespace realm::util { -inline std::string cfstring_to_std_string(CFStringRef cf_str) +inline const char* cfstring_to_cstring(CFStringRef cf_str, std::unique_ptr& buffer) { - std::string ret; // If the CFString happens to store UTF-8 we can read its data directly if (const char* utf8 = CFStringGetCStringPtr(cf_str, kCFStringEncodingUTF8)) { - ret = utf8; - return ret; + return utf8; } // Otherwise we need to convert the CFString to UTF-8 CFIndex length = CFStringGetLength(cf_str); CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; - ret.resize(max_size); - CFStringGetCString(cf_str, &ret[0], max_size, kCFStringEncodingUTF8); - ret.resize(strlen(ret.c_str())); - return ret; + buffer = std::make_unique(max_size); + CFStringGetCString(cf_str, buffer.get(), max_size, kCFStringEncodingUTF8); + return buffer.get(); } -} // namespace util -} // namespace realm +inline CFPtr string_view_to_cfstring(std::string_view string) +{ + if (!string.data()) { + return CFPtr{}; + } + auto result = + adoptCF(CFStringCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast(string.data()), + string.size(), kCFStringEncodingUTF8, false, kCFAllocatorNull)); + if (!result) { + throw std::bad_alloc(); + } + return result; +} + +} // namespace realm::util #endif // REALM_PLATFORM_APPLE diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 94d7f8ad55a..205792be6c2 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -578,6 +578,10 @@ TEST_CASE("C API (non-database)", "[c_api]") { 600024); realm_sync_client_config_set_resumption_delay_backoff_multiplier(test_sync_client_config.get(), 1010); CHECK(test_sync_client_config->timeouts.reconnect_backoff_info.resumption_delay_backoff_multiplier == 1010); +#if REALM_PLATFORM_APPLE + realm_sync_client_config_set_security_access_group(test_sync_client_config.get(), "group.io.realm.test"); + CHECK(test_sync_client_config->security_access_group == "group.io.realm.test"); +#endif } SECTION("realm_app_config_t") { diff --git a/test/object-store/sync/metadata.cpp b/test/object-store/sync/metadata.cpp index 65b76469992..a9703405f1e 100644 --- a/test/object-store/sync/metadata.cpp +++ b/test/object-store/sync/metadata.cpp @@ -32,17 +32,89 @@ #include +#if REALM_PLATFORM_APPLE +#include +#include +#endif + using namespace realm; using namespace realm::util; using File = realm::util::File; using SyncAction = SyncFileActionMetadata::Action; +namespace { static const std::string base_path = util::make_temp_dir() + "realm_objectstore_sync_metadata.test-dir"; static const std::string metadata_path = base_path + "/metadata.realm"; +static const auto no_encryption = [] { + SyncClientConfig config; + config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; + return config; +}(); + +#if REALM_PLATFORM_APPLE +static constexpr const char* app_id = "app id"; +static constexpr const char* access_group = ""; +static bool can_access_keychain() +{ + static bool can_access_keychain = [] { + bool can_access = keychain::create_new_metadata_realm_key(app_id, access_group) != none; + if (can_access) { + keychain::delete_metadata_realm_encryption_key(app_id, access_group); + } + else { + std::cout << "Skipping keychain tests as the keychain is not accessible\n"; + } + return can_access; + }(); + return can_access_keychain; +} + +CFPtr build_search_dictionary(CFStringRef account, CFStringRef service) +{ + auto d = adoptCF( + CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + CFDictionaryAddValue(d.get(), kSecClass, kSecClassGenericPassword); + CFDictionaryAddValue(d.get(), kSecReturnData, kCFBooleanTrue); + CFDictionaryAddValue(d.get(), kSecAttrAccount, account); + CFDictionaryAddValue(d.get(), kSecAttrService, service); + return d; +} + +OSStatus get_key(CFStringRef account, CFStringRef service, std::vector& result) +{ + auto search_dictionary = build_search_dictionary(account, service); + CFDataRef retained_key_data; + OSStatus status = SecItemCopyMatching(search_dictionary.get(), (CFTypeRef*)&retained_key_data); + if (status == errSecSuccess) { + CFPtr key_data = adoptCF(retained_key_data); + auto key_bytes = reinterpret_cast(CFDataGetBytePtr(key_data.get())); + result.assign(key_bytes, key_bytes + CFDataGetLength(key_data.get())); + } + return status; +} + +OSStatus set_key(const std::vector& key, CFStringRef account, CFStringRef service) +{ + auto search_dictionary = build_search_dictionary(account, service); + CFDictionaryAddValue(search_dictionary.get(), kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock); + auto key_data = adoptCF(CFDataCreateWithBytesNoCopy(nullptr, reinterpret_cast(key.data()), + key.size(), kCFAllocatorNull)); + CFDictionaryAddValue(search_dictionary.get(), kSecValueData, key_data.get()); + return SecItemAdd(search_dictionary.get(), nullptr); +} + +std::vector generate_key() +{ + std::vector key(64); + arc4random_buf(key.data(), key.size()); + return key; +} +#endif // REALM_PLATFORM_APPLE +} // anonymous namespace TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); SECTION("can be properly constructed") { const auto identity = "testcase1a"; @@ -128,7 +200,7 @@ TEST_CASE("sync_metadata: user metadata", "[sync][metadata]") { TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); const std::string provider_type = "https://realm.example.org"; SECTION("properly list all marked and unmarked users") { @@ -156,7 +228,7 @@ TEST_CASE("sync_metadata: user metadata APIs", "[sync][metadata]") { TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); const std::string local_uuid_1 = "asdfg"; const std::string local_uuid_2 = "qwerty"; @@ -197,7 +269,7 @@ TEST_CASE("sync_metadata: file action metadata", "[sync][metadata]") { TEST_CASE("sync_metadata: file action metadata APIs", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); SECTION("properly list all pending actions, reflecting their deletion") { const auto filename1 = util::make_temp_dir() + "foobar/file1"; const auto filename2 = util::make_temp_dir() + "foobar/file2"; @@ -219,7 +291,7 @@ TEST_CASE("sync_metadata: file action metadata APIs", "[sync][metadata]") { TEST_CASE("sync_metadata: results", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); const auto identity1 = "testcase3a1"; const auto identity2 = "testcase3a3"; @@ -258,7 +330,7 @@ TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync const auto identity = "testcase4a"; const std::string provider_type = "any-type"; const std::string sample_token = "this_is_a_user_token"; - SyncMetadataManager first_manager(metadata_path, false); + SyncMetadataManager first_manager(metadata_path, no_encryption, "app id"); auto first = first_manager.get_or_make_user_metadata(identity); first->set_access_token(sample_token); REQUIRE(first->identity() == identity); @@ -266,7 +338,7 @@ TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync REQUIRE(first->state() == SyncUser::State::LoggedIn); first->set_state(SyncUser::State::LoggedOut); - SyncMetadataManager second_manager(metadata_path, false); + SyncMetadataManager second_manager(metadata_path, no_encryption, "app id"); auto second = second_manager.get_or_make_user_metadata(identity, false); REQUIRE(second->identity() == identity); REQUIRE(second->access_token() == sample_token); @@ -276,13 +348,14 @@ TEST_CASE("sync_metadata: persistence across metadata manager instances", "[sync TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { test_util::TestDirGuard test_dir(base_path); + SyncClientConfig config; const auto identity0 = "identity0"; SECTION("prohibits opening the metadata Realm with different keys") { SECTION("different keys") { { // Open metadata realm, make metadata - std::vector key0 = make_test_encryption_key(10); - SyncMetadataManager manager0(metadata_path, true, key0); + config.custom_encryption_key = make_test_encryption_key(10); + SyncMetadataManager manager0(metadata_path, config, "app id"); auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); REQUIRE(bool(user_metadata0)); @@ -292,8 +365,8 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { } // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope // Open new metadata realm at path with different key - std::vector key1 = make_test_encryption_key(11); - SyncMetadataManager manager1(metadata_path, true, key1); + config.custom_encryption_key = make_test_encryption_key(11); + SyncMetadataManager manager1(metadata_path, config, "app id"); auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); // Expect previous metadata to have been deleted @@ -309,7 +382,8 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { SECTION("different encryption settings") { { // Encrypt metadata realm at path, make metadata - SyncMetadataManager manager0(metadata_path, true, make_test_encryption_key(10)); + config.custom_encryption_key = make_test_encryption_key(10); + SyncMetadataManager manager0(metadata_path, config, "app id"); auto user_metadata0 = manager0.get_or_make_user_metadata(identity0); REQUIRE(bool(user_metadata0)); @@ -319,7 +393,8 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { } // Metadata realm is closed because only reference to the realm (user_metadata) is now out of scope // Open new metadata realm at path with different encryption configuration - SyncMetadataManager manager1(metadata_path, false); + config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; + SyncMetadataManager manager1(metadata_path, config, "app id"); auto user_metadata1 = manager1.get_or_make_user_metadata(identity0, false); // Expect previous metadata to have been deleted CHECK_FALSE(bool(user_metadata1)); @@ -334,16 +409,16 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { } SECTION("works when enabled") { - std::vector key = make_test_encryption_key(10); + config.custom_encryption_key = make_test_encryption_key(10); const auto identity = "testcase5a"; - SyncMetadataManager manager(metadata_path, true, key); + SyncMetadataManager manager(metadata_path, config, "app id"); auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(bool(user_metadata)); CHECK(user_metadata->identity() == identity); CHECK(user_metadata->access_token().empty()); CHECK(user_metadata->is_valid()); // Reopen the metadata file with the same key. - SyncMetadataManager manager_2(metadata_path, true, key); + SyncMetadataManager manager_2(metadata_path, config, "app id"); auto user_metadata_2 = manager_2.get_or_make_user_metadata(identity, false); REQUIRE(bool(user_metadata_2)); CHECK(user_metadata_2->identity() == identity); @@ -352,29 +427,23 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { SECTION("enabled without custom encryption key") { #if REALM_PLATFORM_APPLE - static bool can_access_keychain = [] { - bool can_access = keychain::create_new_metadata_realm_key() != none; - if (!can_access) { - std::cout << "Skipping keychain tests as the keychain is not accessible\n"; - } - return can_access; - }(); - if (!can_access_keychain) { + if (!can_access_keychain()) { return; } - auto delete_key = util::make_scope_exit([=]() noexcept { - keychain::delete_metadata_realm_encryption_key(); + auto delete_key = util::make_scope_exit([]() noexcept { + keychain::delete_metadata_realm_encryption_key(app_id, access_group); }); + SyncClientConfig config; SECTION("automatically generates an encryption key for new files") { { - SyncMetadataManager manager(metadata_path, true, none); + SyncMetadataManager manager(metadata_path, config, app_id); manager.set_current_user_identity(identity0); } // Should be able to reopen and read data { - SyncMetadataManager manager(metadata_path, true, none); + SyncMetadataManager manager(metadata_path, config, app_id); REQUIRE(manager.get_current_user_identity() == identity0); } @@ -385,11 +454,13 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { SECTION("leaves existing unencrypted files unencrypted") { { - SyncMetadataManager manager(metadata_path, false, none); + config.metadata_mode = SyncClientConfig::MetadataMode::NoEncryption; + SyncMetadataManager manager(metadata_path, config, app_id); manager.set_current_user_identity(identity0); } { - SyncMetadataManager manager(metadata_path, true, none); + config.metadata_mode = SyncClientConfig::MetadataMode::Encryption; + SyncMetadataManager manager(metadata_path, config, app_id); REQUIRE(manager.get_current_user_identity() == identity0); } REQUIRE_NOTHROW(Group(metadata_path)); @@ -397,23 +468,30 @@ TEST_CASE("sync_metadata: encryption", "[sync][metadata]") { SECTION("recreates the file if the old encryption key was lost") { { - SyncMetadataManager manager(metadata_path, true, none); + SyncMetadataManager manager(metadata_path, config, app_id); manager.set_current_user_identity(identity0); } - keychain::delete_metadata_realm_encryption_key(); + keychain::delete_metadata_realm_encryption_key(app_id, access_group); { // File should now be missing the data - SyncMetadataManager manager(metadata_path, true, none); + SyncMetadataManager manager(metadata_path, config, app_id); REQUIRE(manager.get_current_user_identity() == none); } // New file should be encrypted REQUIRE_EXCEPTION(Group(metadata_path), InvalidDatabase, Catch::Matchers::ContainsSubstring("invalid mnemonic")); } + + SECTION("invalid access group throws an error") { + config.security_access_group = "invalid"; + REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, config, app_id), InvalidArgument, + "Invalid access group 'invalid'. Make sure that you have added the access group to " + "your app's Keychain Access Groups Entitlement."); + } #else - REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, true, none), InvalidArgument, + REQUIRE_EXCEPTION(SyncMetadataManager(metadata_path, SyncClientConfig(), "app id"), InvalidArgument, "Metadata Realm encryption was specified, but no encryption key was provided."); #endif } @@ -496,7 +574,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { } #else { // Create a metadata Realm with a test user - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); auto user_metadata = manager.get_or_make_user_metadata(identity); user_metadata->set_access_token(sample_token); } @@ -528,7 +606,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 4") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v4.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->identity() == identity); REQUIRE(user_metadata->access_token() == sample_token); @@ -536,7 +614,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 5") { File::copy(test_util::get_test_resource_path() + "sync-metadata-v5.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); auto user_metadata = manager.get_or_make_user_metadata(identity); REQUIRE(user_metadata->identity() == identity); REQUIRE(user_metadata->access_token() == sample_token); @@ -545,7 +623,7 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { SECTION("open schema version 6") { using State = SyncUser::State; File::copy(test_util::get_test_resource_path() + "sync-metadata-v6.realm", metadata_path); - SyncMetadataManager manager(metadata_path, false); + SyncMetadataManager manager(metadata_path, no_encryption, "app id"); SyncUserIdentity id_1{"identity 1", "a"}; SyncUserIdentity id_2{"identity 2", "b"}; @@ -578,3 +656,93 @@ TEST_CASE("sync metadata: can open old metadata realms", "[sync][metadata]") { } } #endif // SWIFT_PACKAGE + +#if REALM_PLATFORM_APPLE +TEST_CASE("keychain", "[sync][metadata]") { + if (!can_access_keychain()) { + return; + } + auto delete_key = util::make_scope_exit([=]() noexcept { + keychain::delete_metadata_realm_encryption_key(app_id, access_group); + keychain::delete_metadata_realm_encryption_key("app id 1", access_group); + keychain::delete_metadata_realm_encryption_key("app id 2", access_group); + }); + + SECTION("create_new_metadata_realm_key() creates a new key if none exists") { + auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group); + REQUIRE(key_1); + keychain::delete_metadata_realm_encryption_key(app_id, access_group); + auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group); + REQUIRE(key_2); + REQUIRE(key_1 != key_2); + } + + SECTION("create_new_metadata_realm_key() returns the existing one if inserting fails") { + auto key_1 = keychain::create_new_metadata_realm_key(app_id, access_group); + REQUIRE(key_1); + auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group); + REQUIRE(key_2); + REQUIRE(key_1 == key_2); + } + + SECTION("get_existing_metadata_realm_key() returns the key from create_new_metadata_realm_key()") { + auto key_1 = keychain::get_existing_metadata_realm_key(app_id, access_group); + REQUIRE_FALSE(key_1); + auto key_2 = keychain::create_new_metadata_realm_key(app_id, access_group); + REQUIRE(key_2); + auto key_3 = keychain::get_existing_metadata_realm_key(app_id, access_group); + REQUIRE(key_3); + REQUIRE(key_2 == key_3); + } + + SECTION("keys are scoped to app ids") { + auto key_1 = keychain::create_new_metadata_realm_key("app id 1", access_group); + REQUIRE(key_1); + auto key_2 = keychain::create_new_metadata_realm_key("app id 2", access_group); + REQUIRE(key_2); + REQUIRE(key_1 != key_2); + } + + SECTION("legacy key migration") { + auto key = generate_key(); + const auto legacy_account = CFSTR("metadata"); + const auto service_name = CFSTR("io.realm.sync.keychain"); + const auto bundle_id = CFBundleGetIdentifier(CFBundleGetMainBundle()); + // Could be either ObjectStoreTests or CombinedTests but must be set + REQUIRE(bundle_id); + const auto bundle_service = + adoptCF(CFStringCreateWithFormat(nullptr, nullptr, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id)); + + enum class Location { Original, Bundle, BundleAndAppId }; + auto location = GENERATE(Location::Original, Location::Bundle, Location::BundleAndAppId); + CAPTURE(location); + CFStringRef account, service; + switch (location) { + case Location::Original: + account = legacy_account; + service = service_name; + break; + case Location::Bundle: + account = legacy_account; + service = bundle_service.get(); + break; + case Location::BundleAndAppId: + account = CFSTR("app id"); + service = bundle_service.get(); + break; + } + + set_key(key, account, service); + auto key_2 = keychain::get_existing_metadata_realm_key(app_id, {}); + REQUIRE(key_2 == key); + + // Key should have been copied to the preferred location + REQUIRE(get_key(CFSTR("app id"), bundle_service.get(), key) == errSecSuccess); + REQUIRE(key_2 == key); + + // Key should not have been deleted from the original location + REQUIRE(get_key(account, service, key) == errSecSuccess); + REQUIRE(key_2 == key); + } +} +#endif diff --git a/test/object-store/sync/sync_manager.cpp b/test/object-store/sync/sync_manager.cpp index 57bb908cbc1..2894ea8bd6f 100644 --- a/test/object-store/sync/sync_manager.cpp +++ b/test/object-store/sync/sync_manager.cpp @@ -271,7 +271,9 @@ TEST_CASE("sync_manager: persistent user state management", "[sync][sync manager config.should_teardown_test_directory = false; auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); + SyncClientConfig client_config; + client_config.metadata_mode = config.metadata_mode; + SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); const std::string r_token_1 = ENCODE_FAKE_JWT("foo_token"); const std::string r_token_2 = ENCODE_FAKE_JWT("bar_token"); @@ -447,7 +449,9 @@ TEST_CASE("sync_manager: file actions", "[sync][sync manager]") { auto file_manager = SyncFileManager(base_path.string(), "app_id"); // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); + SyncClientConfig client_config; + client_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; + SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); TestSyncManager::Config config; config.base_path = base_path.string(); diff --git a/test/object-store/sync/user.cpp b/test/object-store/sync/user.cpp index 57c0189d258..de3f697d2de 100644 --- a/test/object-store/sync/user.cpp +++ b/test/object-store/sync/user.cpp @@ -186,7 +186,9 @@ TEST_CASE("sync_user: user persistence", "[sync][user]") { auto sync_manager = tsm.sync_manager(); auto file_manager = SyncFileManager(tsm.base_file_path(), "app_id"); // Open the metadata separately, so we can investigate it ourselves. - SyncMetadataManager manager(file_manager.metadata_path(), false); + SyncClientConfig client_config; + client_config.metadata_mode = tsm_config.metadata_mode; + SyncMetadataManager manager(file_manager.metadata_path(), client_config, "app_id"); SECTION("properly persists a user's information upon creation") { const std::string identity = "test_identity_1";