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