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