• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2275

29 Apr 2024 10:47PM UTC coverage: 90.753% (+0.04%) from 90.709%
2275

push

Evergreen

web-flow
Bump minimum deployment targets to the minimums supported by Xcode 15 (#7648)

Submitting apps built with Xcode 14 to the app store is no longer allowed, so
we no longer support building with Xcode 14.

101920 of 180232 branches covered (56.55%)

11 of 22 new or added lines in 4 files covered. (50.0%)

37 existing lines in 8 files now uncovered.

212491 of 234141 relevant lines covered (90.75%)

5962906.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.3
/src/realm/object-store/impl/apple/keychain_helper.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2016 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/impl/apple/keychain_helper.hpp>
20

21
#include <realm/exceptions.hpp>
22
#include <realm/util/cf_str.hpp>
23

24
#include <Security/Security.h>
25

26
#include <string>
27

28
using namespace realm;
29
using util::adoptCF;
30
using util::CFPtr;
31
using util::string_view_to_cfstring;
32

33
namespace {
34

35
REALM_NORETURN
36
REALM_COLD
37
void keychain_access_exception(int32_t error_code)
38
{
×
NEW
39
    if (auto message = adoptCF(SecCopyErrorMessageString(error_code, nullptr))) {
×
NEW
40
        if (auto msg = CFStringGetCStringPtr(message.get(), kCFStringEncodingUTF8)) {
×
NEW
41
            throw RuntimeError(ErrorCodes::RuntimeError,
×
NEW
42
                               util::format("Keychain returned unexpected status code: %1 (%2)", msg, error_code));
×
NEW
43
        }
×
NEW
44
        auto length = CFStringGetMaximumSizeForEncoding(CFStringGetLength(message.get()), kCFStringEncodingUTF8) + 1;
×
NEW
45
        auto buffer = std::make_unique<char[]>(length);
×
NEW
46
        if (CFStringGetCString(message.get(), buffer.get(), length, kCFStringEncodingUTF8)) {
×
NEW
47
            throw RuntimeError(
×
NEW
48
                ErrorCodes::RuntimeError,
×
NEW
49
                util::format("Keychain returned unexpected status code: %1 (%2)", buffer.get(), error_code));
×
50
        }
×
51
    }
×
52
    throw RuntimeError(ErrorCodes::RuntimeError,
×
53
                       util::format("Keychain returned unexpected status code: %1", error_code));
×
54
}
×
55

56
constexpr size_t key_size = 64;
57
const CFStringRef s_legacy_account = CFSTR("metadata");
58
const CFStringRef s_service = CFSTR("io.realm.sync.keychain");
59

60
CFPtr<CFMutableDictionaryRef> build_search_dictionary(CFStringRef account, CFStringRef service, CFStringRef group)
61
{
1✔
62
    auto d = adoptCF(
1✔
63
        CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
1✔
64
    if (!d)
1✔
65
        throw std::bad_alloc();
×
66

67
    CFDictionaryAddValue(d.get(), kSecClass, kSecClassGenericPassword);
1✔
68
    CFDictionaryAddValue(d.get(), kSecReturnData, kCFBooleanTrue);
1✔
69
    CFDictionaryAddValue(d.get(), kSecAttrAccount, account);
1✔
70
    CFDictionaryAddValue(d.get(), kSecAttrService, service);
1✔
71
    if (group) {
1✔
72
        CFDictionaryAddValue(d.get(), kSecAttrAccessGroup, group);
×
73
        if (__builtin_available(macOS 10.15, iOS 13.0, *)) {
×
74
            CFDictionaryAddValue(d.get(), kSecUseDataProtectionKeychain, kCFBooleanTrue);
×
75
        }
×
76
    }
×
77
    return d;
1✔
78
}
1✔
79

80
/// Get the encryption key for a given service, returning true if it either exists or the keychain is not usable.
81
bool get_key(CFStringRef account, CFStringRef service, std::string_view group,
82
             std::optional<std::vector<char>>& result, bool result_on_error = true)
83
{
×
84
    auto search_dictionary = build_search_dictionary(account, service, string_view_to_cfstring(group).get());
×
85
    CFDataRef retained_key_data;
×
86
    switch (OSStatus status = SecItemCopyMatching(search_dictionary.get(), (CFTypeRef*)&retained_key_data)) {
×
87
        case errSecSuccess: {
×
88
            // Key was previously stored. Extract it.
89
            CFPtr<CFDataRef> key_data = adoptCF(retained_key_data);
×
90
            if (key_size != CFDataGetLength(key_data.get()))
×
91
                return false;
×
92

93
            auto key_bytes = reinterpret_cast<const char*>(CFDataGetBytePtr(key_data.get()));
×
94
            result.emplace(key_bytes, key_bytes + key_size);
×
95
            return true;
×
96
        }
×
97
        case errSecItemNotFound:
×
98
            return false;
×
99
        case errSecUserCanceled:
×
100
            // Keychain is locked, and user did not enter the password to unlock it.
101
        case errSecInvalidKeychain:
×
102
            // The keychain is corrupted and cannot be used.
103
        case errSecNotAvailable:
×
104
            // There are no keychain files.
105
        case errSecInteractionNotAllowed:
×
106
            // We asked for it to not prompt the user and a prompt was needed
107
            return result_on_error;
×
108
        case errSecMissingEntitlement:
×
109
            throw InvalidArgument(util::format("Invalid access group '%1'. Make sure that you have added the access "
×
110
                                               "group to your app's Keychain Access Groups Entitlement.",
×
111
                                               group));
×
112
        default:
×
113
            keychain_access_exception(status);
×
114
    }
×
115
}
×
116

117
bool set_key(std::optional<std::vector<char>>& key, CFStringRef account, CFStringRef service, std::string_view group)
118
{
1✔
119
    // key may be nullopt here if the keychain was inaccessible
120
    if (!key)
1✔
121
        return false;
×
122

123
    auto search_dictionary = build_search_dictionary(account, service, string_view_to_cfstring(group).get());
1✔
124
    CFDictionaryAddValue(search_dictionary.get(), kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock);
1✔
125
    auto key_data = adoptCF(CFDataCreateWithBytesNoCopy(nullptr, reinterpret_cast<const UInt8*>(key->data()),
1✔
126
                                                        key_size, kCFAllocatorNull));
1✔
127
    if (!key_data)
1✔
128
        throw std::bad_alloc();
×
129

130
    CFDictionaryAddValue(search_dictionary.get(), kSecValueData, key_data.get());
1✔
131
    switch (OSStatus status = SecItemAdd(search_dictionary.get(), nullptr)) {
1✔
132
        case errSecSuccess:
✔
133
            return true;
×
134
        case errSecDuplicateItem:
✔
135
            // A keychain item already exists but we didn't find it in get_key().
136
            // Either someone else created it between when we last checked and
137
            // now or we don't have permission to read it. Try to reread the key
138
            // and discard the one we just created in case it's the former
139
            if (get_key(account, service, group, key, false))
×
140
                return true;
×
141
        case errSecMissingEntitlement:
✔
142
        case errSecUserCanceled:
✔
143
        case errSecInteractionNotAllowed:
1✔
144
        case errSecInvalidKeychain:
1✔
145
        case errSecNotAvailable:
1✔
146
            // We were unable to save the key for "expected" reasons, so proceed unencrypted
147
            return false;
1✔
148
        default:
✔
149
            // Unexpected keychain failure happened
150
            keychain_access_exception(status);
×
151
    }
1✔
152
}
1✔
153

154
void delete_key(CFStringRef account, CFStringRef service, CFStringRef group)
155
{
×
156
    auto search_dictionary = build_search_dictionary(account, service, group);
×
157
    auto status = SecItemDelete(search_dictionary.get());
×
158
    REALM_ASSERT(status == errSecSuccess || status == errSecItemNotFound);
×
159
}
×
160

161
CFPtr<CFStringRef> bundle_service()
162
{
1✔
163
    if (CFStringRef bundle_id = CFBundleGetIdentifier(CFBundleGetMainBundle())) {
1✔
164
        return adoptCF(CFStringCreateWithFormat(nullptr, nullptr, CFSTR("%@ - Realm Sync Metadata Key"), bundle_id));
1✔
165
    }
1✔
166
    return CFPtr<CFStringRef>{};
×
167
}
1✔
168

169
} // anonymous namespace
170

171
namespace realm::keychain {
172

173
std::optional<std::vector<char>> get_existing_metadata_realm_key(std::string_view app_id,
174
                                                                 std::string_view access_group)
175
{
×
176
    auto cf_app_id = string_view_to_cfstring(app_id);
×
177
    std::optional<std::vector<char>> key;
×
178

179
    // If we have a security access groups then keys are stored the same way
180
    // everywhere and we don't have any legacy storage methods to handle, so
181
    // we just either have a key or we don't.
182
    if (access_group.size()) {
×
183
        get_key(cf_app_id.get(), s_service, access_group, key);
×
184
        return key;
×
185
    }
×
186

187
    // When we don't have an access group we check a whole bunch of things because
188
    // there's been a variety of ways that we've stored metadata keys over the years.
189
    // If we find a key stored in a non-preferred way we copy it to the preferred
190
    // location before returning it.
191
    //
192
    // The original location was (account: "metadata", service: "io.realm.sync.keychain").
193
    // For processes with a bundle ID, we then switched to (account: "metadata",
194
    // service: "$bundleId - Realm Sync Metadata Key")
195
    // The current preferred location on non-macOS (account: appId, service: "io.realm.sync.keychain"),
196
    // and on macOS is (account: appId, service: "$bundleId - Realm Sync Metadata Key").
197
    //
198
    // On everything but macOS the keychain is scoped to the app, so there's no
199
    // need to include the bundle ID. On macOS it's user-wide, and we want each
200
    // application using Realm to have separate state. Using multiple server apps
201
    // in one client is unusual, but when it's done we want each metadata realm to
202
    // have a separate key.
203

204
#if TARGET_OS_MAC
×
205
    if (auto service = bundle_service()) {
×
206
        if (get_key(cf_app_id.get(), service.get(), {}, key))
×
207
            return key;
×
208
        if (get_key(s_legacy_account, service.get(), {}, key)) {
×
209
            set_key(key, cf_app_id.get(), service.get(), {});
×
210
            return key;
×
211
        }
×
212
        if (get_key(s_legacy_account, s_service, {}, key)) {
×
213
            set_key(key, cf_app_id.get(), service.get(), {});
×
214
            return key;
×
215
        }
×
216
    }
×
217
    else {
×
218
        if (get_key(cf_app_id.get(), s_service, {}, key))
×
219
            return key;
×
220
        if (get_key(s_legacy_account, s_service, {}, key)) {
×
221
            set_key(key, cf_app_id.get(), s_service, {});
×
222
            return key;
×
223
        }
×
224
    }
×
225
#else
226
    if (get_key(cf_app_id, s_service, {}, key))
227
        return key;
228
    if (auto service = bundle_service()) {
229
        if (get_key(cf_app_id, service, {}, key)) {
230
            set_key(key, cf_app_id, s_service, {});
231
            return key;
232
        }
233
    }
234
    if (get_key(s_legacy_account, s_service, {}, key)) {
235
        set_key(key, cf_app_id, s_service, {});
236
        return key;
237
    }
238
#endif
239

240
    return key;
×
241
}
×
242

243
std::optional<std::vector<char>> create_new_metadata_realm_key(std::string_view app_id, std::string_view access_group)
244
{
1✔
245
    auto cf_app_id = string_view_to_cfstring(app_id);
1✔
246
    std::optional<std::vector<char>> key;
1✔
247
    key.emplace(key_size);
1✔
248
    arc4random_buf(key->data(), key_size);
1✔
249

250
    // See above for why macOS is different
251
#if TARGET_OS_OSX
1✔
252
    if (!access_group.size()) {
1✔
253
        if (auto service = bundle_service()) {
1✔
254
            if (!set_key(key, cf_app_id.get(), service.get(), {}))
1✔
255
                key.reset();
1✔
256
            return key;
1✔
257
        }
1✔
258
    }
1✔
259
#endif
×
260

261
    // If we're unable to save the newly created key, clear it and proceed unencrypted
262
    if (!set_key(key, cf_app_id.get(), s_service, access_group))
×
263
        key.reset();
×
264
    return key;
×
265
}
1✔
266

267
void delete_metadata_realm_encryption_key(std::string_view app_id, std::string_view access_group)
268
{
×
269
    auto cf_app_id = string_view_to_cfstring(app_id);
×
270
    if (access_group.size()) {
×
271
        delete_key(cf_app_id.get(), s_service, string_view_to_cfstring(access_group).get());
×
272
        return;
×
273
    }
×
274

275
    delete_key(cf_app_id.get(), s_service, {});
×
276
    delete_key(s_legacy_account, s_service, {});
×
277
    if (auto service = bundle_service()) {
×
278
        delete_key(cf_app_id.get(), service.get(), {});
×
279
        delete_key(s_legacy_account, service.get(), {});
×
280
    }
×
281
}
×
282

283
} // namespace realm::keychain
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc