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

realm / realm-core / 2216

11 Apr 2024 03:58PM UTC coverage: 91.787% (-0.03%) from 91.813%
2216

push

Evergreen

web-flow
Use the correct target conditional (#7579)

94858 of 175770 branches covered (53.97%)

0 of 1 new or added line in 1 file covered. (0.0%)

162 existing lines in 19 files now uncovered.

242852 of 264583 relevant lines covered (91.79%)

5469917.69 hits per line

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

25.77
/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
{
×
39
    if (__builtin_available(iOS 11.3, macOS 10.3, tvOS 11.3, watchOS 4.3, *)) {
×
40
        if (auto message = adoptCF(SecCopyErrorMessageString(error_code, nullptr))) {
×
41
            if (auto msg = CFStringGetCStringPtr(message.get(), kCFStringEncodingUTF8)) {
×
42
                throw RuntimeError(
×
43
                    ErrorCodes::RuntimeError,
×
44
                    util::format("Keychain returned unexpected status code: %1 (%2)", msg, error_code));
×
45
            }
×
46
            auto length =
×
47
                CFStringGetMaximumSizeForEncoding(CFStringGetLength(message.get()), kCFStringEncodingUTF8) + 1;
×
48
            auto buffer = std::make_unique<char[]>(length);
×
49
            if (CFStringGetCString(message.get(), buffer.get(), length, kCFStringEncodingUTF8)) {
×
50
                throw RuntimeError(
×
51
                    ErrorCodes::RuntimeError,
×
52
                    util::format("Keychain returned unexpected status code: %1 (%2)", buffer.get(), error_code));
×
53
            }
×
54
        }
×
55
    }
×
56
    throw RuntimeError(ErrorCodes::RuntimeError,
×
57
                       util::format("Keychain returned unexpected status code: %1", error_code));
×
58
}
×
59

60
constexpr size_t key_size = 64;
61
const CFStringRef s_legacy_account = CFSTR("metadata");
62
const CFStringRef s_service = CFSTR("io.realm.sync.keychain");
63

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

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

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

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

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

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

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

158
void delete_key(CFStringRef account, CFStringRef service, CFStringRef group)
159
{
×
160
    auto search_dictionary = build_search_dictionary(account, service, group);
×
161
    auto status = SecItemDelete(search_dictionary.get());
×
162
    REALM_ASSERT(status == errSecSuccess || status == errSecItemNotFound);
×
163
}
×
164

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

173
} // anonymous namespace
174

175
namespace realm::keychain {
176

177
std::optional<std::vector<char>> get_existing_metadata_realm_key(std::string_view app_id,
178
                                                                 std::string_view access_group)
179
{
×
180
    auto cf_app_id = string_view_to_cfstring(app_id);
×
181
    std::optional<std::vector<char>> key;
×
182

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

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

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

244
    return key;
×
245
}
×
246

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

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

265
    // If we're unable to save the newly created key, clear it and proceed unencrypted
266
    if (!set_key(key, cf_app_id.get(), s_service, access_group))
×
267
        key.reset();
×
268
    return key;
×
269
}
×
270

271
void delete_metadata_realm_encryption_key(std::string_view app_id, std::string_view access_group)
272
{
×
273
    auto cf_app_id = string_view_to_cfstring(app_id);
×
274
    if (access_group.size()) {
×
275
        delete_key(cf_app_id.get(), s_service, string_view_to_cfstring(access_group).get());
×
276
        return;
×
277
    }
×
278

279
    delete_key(cf_app_id.get(), s_service, {});
×
280
    delete_key(s_legacy_account, s_service, {});
×
281
    if (auto service = bundle_service()) {
×
282
        delete_key(cf_app_id.get(), service.get(), {});
×
283
        delete_key(s_legacy_account, service.get(), {});
×
284
    }
×
285
}
×
286

287
} // 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