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