xref: /aosp_15_r20/external/cronet/crypto/fake_apple_keychain_v2.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1// Copyright 2024 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "crypto/fake_apple_keychain_v2.h"
6
7#include <vector>
8
9#if defined(LEAK_SANITIZER)
10#include <sanitizer/lsan_interface.h>
11#endif
12
13#import <CoreFoundation/CoreFoundation.h>
14#import <Foundation/Foundation.h>
15#import <LocalAuthentication/LocalAuthentication.h>
16#import <Security/Security.h>
17
18#include "base/apple/bridging.h"
19#include "base/apple/foundation_util.h"
20#include "base/apple/scoped_cftyperef.h"
21#include "base/check_op.h"
22#include "base/notimplemented.h"
23#include "base/notreached.h"
24#include "base/strings/sys_string_conversions.h"
25#include "crypto/apple_keychain_v2.h"
26
27namespace crypto {
28
29FakeAppleKeychainV2::FakeAppleKeychainV2(
30    const std::string& keychain_access_group)
31    : keychain_access_group_(
32          base::SysUTF8ToCFStringRef(keychain_access_group)) {}
33FakeAppleKeychainV2::~FakeAppleKeychainV2() {
34  // Avoid shutdown leak of error string in Security.framework.
35  // See
36  // https://github.com/apple-oss-distributions/Security/blob/Security-60158.140.3/OSX/libsecurity_keychain/lib/SecBase.cpp#L88
37#if defined(LEAK_SANITIZER)
38  __lsan_do_leak_check();
39#endif
40}
41
42NSArray* FakeAppleKeychainV2::GetTokenIDs() {
43  if (is_secure_enclave_available_) {
44    return @[ base::apple::CFToNSPtrCast(kSecAttrTokenIDSecureEnclave) ];
45  }
46  return @[];
47}
48
49base::apple::ScopedCFTypeRef<SecKeyRef> FakeAppleKeychainV2::KeyCreateRandomKey(
50    CFDictionaryRef params,
51    CFErrorRef* error) {
52  // Validate certain fields that we always expect to be set.
53  DCHECK(
54      base::apple::GetValueFromDictionary<CFStringRef>(params, kSecAttrLabel));
55  // kSecAttrApplicationTag is CFDataRef for new credentials and CFStringRef for
56  // version < 3. Keychain docs say it should be CFDataRef
57  // (https://developer.apple.com/documentation/security/ksecattrapplicationtag).
58  CFTypeRef application_tag = nil;
59  CFDictionaryGetValueIfPresent(params, kSecAttrApplicationTag,
60                                &application_tag);
61  if (application_tag) {
62    CHECK(base::apple::CFCast<CFDataRef>(application_tag) ||
63          base::apple::CFCast<CFStringRef>(application_tag));
64  }
65  DCHECK_EQ(
66      base::apple::GetValueFromDictionary<CFStringRef>(params, kSecAttrTokenID),
67      kSecAttrTokenIDSecureEnclave);
68  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
69                     params, kSecAttrAccessGroup),
70                 keychain_access_group_.get()));
71
72  // Call Keychain services to create a key pair, but first drop all parameters
73  // that aren't appropriate in tests.
74  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> params_copy(
75      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
76                                    params));
77  // Don't create a Secure Enclave key.
78  CFDictionaryRemoveValue(params_copy.get(), kSecAttrTokenID);
79  // Don't bind to a keychain-access-group, which would require an entitlement.
80  CFDictionaryRemoveValue(params_copy.get(), kSecAttrAccessGroup);
81
82  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> private_key_params(
83      CFDictionaryCreateMutableCopy(
84          kCFAllocatorDefault, /*capacity=*/0,
85          base::apple::GetValueFromDictionary<CFDictionaryRef>(
86              params_copy.get(), kSecPrivateKeyAttrs)));
87  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFBooleanRef>(
88                     private_key_params.get(), kSecAttrIsPermanent),
89                 kCFBooleanTrue));
90  CFDictionarySetValue(private_key_params.get(), kSecAttrIsPermanent,
91                       kCFBooleanFalse);
92  CFDictionaryRemoveValue(private_key_params.get(), kSecAttrAccessControl);
93  CFDictionaryRemoveValue(private_key_params.get(),
94                          kSecUseAuthenticationContext);
95  CFDictionarySetValue(params_copy.get(), kSecPrivateKeyAttrs,
96                       private_key_params.get());
97  base::apple::ScopedCFTypeRef<SecKeyRef> private_key(
98      SecKeyCreateRandomKey(params_copy.get(), error));
99  if (!private_key) {
100    return base::apple::ScopedCFTypeRef<SecKeyRef>();
101  }
102
103  // Stash everything in `items_` so it can be  retrieved in with
104  // `ItemCopyMatching. This uses the original `params` rather than the modified
105  // copy so that `ItemCopyMatching()` will correctly filter on
106  // kSecAttrAccessGroup.
107  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> keychain_item(
108      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
109                                    params));
110  CFDictionarySetValue(keychain_item.get(), kSecValueRef, private_key.get());
111
112  // When left unset, the real keychain sets the application label to the hash
113  // of the public key on creation. We need to retrieve it to allow filtering
114  // for it later.
115  if (!base::apple::GetValueFromDictionary<CFDataRef>(
116          keychain_item.get(), kSecAttrApplicationLabel)) {
117    base::apple::ScopedCFTypeRef<CFDictionaryRef> key_metadata(
118        SecKeyCopyAttributes(private_key.get()));
119    CFDataRef application_label =
120        base::apple::GetValueFromDictionary<CFDataRef>(
121            key_metadata.get(), kSecAttrApplicationLabel);
122    CFDictionarySetValue(keychain_item.get(), kSecAttrApplicationLabel,
123                         application_label);
124  }
125  items_.push_back(keychain_item);
126
127  return private_key;
128}
129
130base::apple::ScopedCFTypeRef<CFDictionaryRef>
131FakeAppleKeychainV2::KeyCopyAttributes(SecKeyRef key) {
132  const auto& it = std::ranges::find_if(items_, [&key](const auto& item) {
133    return CFEqual(key, CFDictionaryGetValue(item.get(), kSecValueRef));
134  });
135  if (it == items_.end()) {
136    return base::apple::ScopedCFTypeRef<CFDictionaryRef>();
137  }
138  base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> result(
139      CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
140                                    it->get()));
141  // The real implementation does not return the actual key.
142  CFDictionaryRemoveValue(result.get(), kSecValueRef);
143  return result;
144}
145
146OSStatus FakeAppleKeychainV2::ItemCopyMatching(CFDictionaryRef query,
147                                               CFTypeRef* result) {
148  // In practice we don't need to care about limit queries, or leaving out the
149  // SecKeyRef or attributes from the result set.
150  DCHECK_EQ(
151      base::apple::GetValueFromDictionary<CFBooleanRef>(query, kSecReturnRef),
152      kCFBooleanTrue);
153  DCHECK_EQ(base::apple::GetValueFromDictionary<CFBooleanRef>(
154                query, kSecReturnAttributes),
155            kCFBooleanTrue);
156  CFStringRef match_limit =
157      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecMatchLimit);
158  bool match_all = match_limit && CFEqual(match_limit, kSecMatchLimitAll);
159
160  // Match fields present in `query`.
161  CFStringRef query_label =
162      base::apple::GetValueFromDictionary<CFStringRef>(query, kSecAttrLabel);
163  CFDataRef query_application_label =
164      base::apple::GetValueFromDictionary<CFDataRef>(query,
165                                                     kSecAttrApplicationLabel);
166  // kSecAttrApplicationTag can be CFStringRef for legacy credentials and
167  // CFDataRef for new ones, hence using CFTypeRef.
168  CFTypeRef query_application_tag =
169      CFDictionaryGetValue(query, kSecAttrApplicationTag);
170
171  // Filter the items based on `query`.
172  base::apple::ScopedCFTypeRef<CFMutableArrayRef> items(
173      CFArrayCreateMutable(nullptr, items_.size(), &kCFTypeArrayCallBacks));
174  for (auto& item : items_) {
175    // Each `Keychain` instance is expected to operate only on items of a single
176    // keychain-access-group, which is tied to the `Profile`.
177    CFStringRef keychain_access_group =
178        base::apple::GetValueFromDictionary<CFStringRef>(query,
179                                                         kSecAttrAccessGroup);
180    DCHECK(CFEqual(keychain_access_group,
181                   base::apple::GetValueFromDictionary<CFStringRef>(
182                       item.get(), kSecAttrAccessGroup)) &&
183           CFEqual(keychain_access_group, keychain_access_group_.get()));
184
185    CFStringRef item_label = base::apple::GetValueFromDictionary<CFStringRef>(
186        item.get(), kSecAttrLabel);
187    CFDataRef item_application_label =
188        base::apple::GetValueFromDictionary<CFDataRef>(
189            item.get(), kSecAttrApplicationLabel);
190    CFTypeRef item_application_tag =
191        CFDictionaryGetValue(item.get(), kSecAttrApplicationTag);
192    if ((query_label && (!item_label || !CFEqual(query_label, item_label))) ||
193        (query_application_label &&
194         (!item_application_label ||
195          !CFEqual(query_application_label, item_application_label))) ||
196        (query_application_tag &&
197         (!item_application_tag ||
198          !CFEqual(query_application_tag, item_application_tag)))) {
199      continue;
200    }
201    if (match_all) {
202      base::apple::ScopedCFTypeRef<CFDictionaryRef> item_copy(
203          CFDictionaryCreateCopy(kCFAllocatorDefault, item.get()));
204      CFArrayAppendValue(items.get(), item_copy.get());
205    } else {
206      *result = CFDictionaryCreateCopy(kCFAllocatorDefault, item.get());
207      return errSecSuccess;
208    }
209  }
210  if (CFArrayGetCount(items.get()) == 0) {
211    return errSecItemNotFound;
212  }
213  *result = items.release();
214  return errSecSuccess;
215}
216
217OSStatus FakeAppleKeychainV2::ItemDelete(CFDictionaryRef query) {
218  // Validate certain fields that we always expect to be set.
219  DCHECK_EQ(base::apple::GetValueFromDictionary<CFStringRef>(query, kSecClass),
220            kSecClassKey);
221  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
222                     query, kSecAttrAccessGroup),
223                 keychain_access_group_.get()));
224  // Only supporting deletion via `kSecAttrApplicationLabel` (credential ID) for
225  // now (see `TouchIdCredentialStore::DeleteCredentialById()`).
226  CFDataRef query_credential_id =
227      base::apple::GetValueFromDictionary<CFDataRef>(query,
228                                                     kSecAttrApplicationLabel);
229  DCHECK(query_credential_id);
230  for (auto it = items_.begin(); it != items_.end(); ++it) {
231    const base::apple::ScopedCFTypeRef<CFDictionaryRef>& item = *it;
232    CFDataRef item_credential_id =
233        base::apple::GetValueFromDictionary<CFDataRef>(
234            item.get(), kSecAttrApplicationLabel);
235    DCHECK(item_credential_id);
236    if (CFEqual(query_credential_id, item_credential_id)) {
237      items_.erase(it);  // N.B. `it` becomes invalid
238      return errSecSuccess;
239    }
240  }
241  return errSecItemNotFound;
242}
243
244OSStatus FakeAppleKeychainV2::ItemUpdate(CFDictionaryRef query,
245                                         CFDictionaryRef attributes_to_update) {
246  DCHECK_EQ(base::apple::GetValueFromDictionary<CFStringRef>(query, kSecClass),
247            kSecClassKey);
248  DCHECK(CFEqual(base::apple::GetValueFromDictionary<CFStringRef>(
249                     query, kSecAttrAccessGroup),
250                 keychain_access_group_.get()));
251  CFDataRef query_credential_id =
252      base::apple::GetValueFromDictionary<CFDataRef>(query,
253                                                     kSecAttrApplicationLabel);
254  DCHECK(query_credential_id);
255  for (base::apple::ScopedCFTypeRef<CFDictionaryRef>& item : items_) {
256    CFDataRef item_credential_id =
257        base::apple::GetValueFromDictionary<CFDataRef>(
258            item.get(), kSecAttrApplicationLabel);
259    DCHECK(item_credential_id);
260    if (!CFEqual(query_credential_id, item_credential_id)) {
261      continue;
262    }
263    base::apple::ScopedCFTypeRef<CFMutableDictionaryRef> item_copy(
264        CFDictionaryCreateMutableCopy(kCFAllocatorDefault, /*capacity=*/0,
265                                      item.get()));
266    [base::apple::CFToNSPtrCast(item_copy.get())
267        addEntriesFromDictionary:base::apple::CFToNSPtrCast(
268                                     attributes_to_update)];
269    item = item_copy;
270    return errSecSuccess;
271  }
272  return errSecItemNotFound;
273}
274
275#if !BUILDFLAG(IS_IOS)
276base::apple::ScopedCFTypeRef<CFTypeRef>
277FakeAppleKeychainV2::TaskCopyValueForEntitlement(SecTaskRef task,
278                                                 CFStringRef entitlement,
279                                                 CFErrorRef* error) {
280  CHECK(task);
281  CHECK(CFEqual(entitlement,
282                base::SysUTF8ToCFStringRef("keychain-access-groups").get()))
283      << "Entitlement " << entitlement << " not supported by fake";
284  base::apple::ScopedCFTypeRef<CFMutableArrayRef> keychain_access_groups(
285      CFArrayCreateMutable(kCFAllocatorDefault, /*capacity=*/1,
286                           &kCFTypeArrayCallBacks));
287  CFArrayAppendValue(
288      keychain_access_groups.get(),
289      CFStringCreateCopy(kCFAllocatorDefault, keychain_access_group_.get()));
290  return keychain_access_groups;
291}
292#endif  // !BUILDFLAG(IS_IOS)
293
294BOOL FakeAppleKeychainV2::LAContextCanEvaluatePolicy(
295    LAPolicy policy,
296    NSError* __autoreleasing* error) {
297  switch (policy) {
298    case LAPolicyDeviceOwnerAuthentication:
299      return uv_method_ == UVMethod::kBiometrics ||
300             uv_method_ == UVMethod::kPasswordOnly;
301    case LAPolicyDeviceOwnerAuthenticationWithBiometrics:
302      return uv_method_ == UVMethod::kBiometrics;
303    case LAPolicyDeviceOwnerAuthenticationWithBiometricsOrWatch:
304      return uv_method_ == UVMethod::kBiometrics;
305    default:  // Avoid needing to refer to values not available in the minimum
306              // supported macOS version.
307      NOTIMPLEMENTED();
308      return false;
309  }
310}
311
312}  // namespace crypto
313