SFKeychainControlManager.m
1 /* 2 * Copyright (c) 2017 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24 #import "SFKeychainControlManager.h" 25 #import "SecCFError.h" 26 #import "builtin_commands.h" 27 #import "debugging.h" 28 #import <Security/SecItem.h> 29 #import <Security/SecItemPriv.h> 30 #import <Foundation/NSXPCConnection_Private.h> 31 #import <Security/SecXPCHelper.h> 32 33 NSString* kSecEntitlementKeychainControl = @"com.apple.private.keychain.keychaincontrol"; 34 35 XPC_RETURNS_RETAINED xpc_endpoint_t SecServerCreateKeychainControlEndpoint(void) 36 { 37 return [[SFKeychainControlManager sharedManager] xpcControlEndpoint]; 38 } 39 40 @implementation SFKeychainControlManager { 41 NSXPCListener* _listener; 42 } 43 44 + (instancetype)sharedManager 45 { 46 static SFKeychainControlManager* manager = nil; 47 static dispatch_once_t onceToken; 48 dispatch_once(&onceToken, ^{ 49 manager = [[SFKeychainControlManager alloc] _init]; 50 }); 51 52 return manager; 53 } 54 55 - (instancetype)_init 56 { 57 if (self = [super init]) { 58 _listener = [NSXPCListener anonymousListener]; 59 _listener.delegate = self; 60 [_listener resume]; 61 } 62 63 return self; 64 } 65 66 - (xpc_endpoint_t)xpcControlEndpoint 67 { 68 return [_listener.endpoint _endpoint]; 69 } 70 71 - (BOOL)listener:(NSXPCListener*)listener shouldAcceptNewConnection:(NSXPCConnection*)newConnection 72 { 73 NSNumber* entitlementValue = [newConnection valueForEntitlement:kSecEntitlementKeychainControl]; 74 if (![entitlementValue isKindOfClass:[NSNumber class]] || !entitlementValue.boolValue) { 75 secerror("SFKeychainControl: Client pid (%d) doesn't have entitlement: %@", newConnection.processIdentifier, kSecEntitlementKeychainControl); 76 return NO; 77 } 78 79 NSSet<Class>* errorClasses = [SecXPCHelper safeErrorClasses]; 80 81 NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(SFKeychainControl)]; 82 [interface setClasses:errorClasses forSelector:@selector(rpcFindCorruptedItemsWithReply:) argumentIndex:1 ofReply:YES]; 83 [interface setClasses:errorClasses forSelector:@selector(rpcDeleteCorruptedItemsWithReply:) argumentIndex:1 ofReply:YES]; 84 newConnection.exportedInterface = interface; 85 newConnection.exportedObject = self; 86 [newConnection resume]; 87 return YES; 88 } 89 90 - (NSArray<NSDictionary*>*)findCorruptedItemsWithError:(NSError**)error 91 { 92 NSMutableArray<NSDictionary*>* corruptedItems = [[NSMutableArray alloc] init]; 93 NSMutableArray* underlyingErrors = [[NSMutableArray alloc] init]; 94 95 CFTypeRef genericPasswords = NULL; 96 NSDictionary* genericPasswordsQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword, 97 (id)kSecReturnPersistentRef : @(YES), 98 (id)kSecUseDataProtectionKeychain : @(YES), 99 (id)kSecMatchLimit : (id)kSecMatchLimitAll }; 100 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordsQuery, &genericPasswords); 101 CFErrorRef genericPasswordError = NULL; 102 if (status != errSecItemNotFound) { 103 SecError(status, &genericPasswordError, CFSTR("generic password query failed")); 104 if (genericPasswordError) { 105 [underlyingErrors addObject:CFBridgingRelease(genericPasswordError)]; 106 } 107 } 108 109 CFTypeRef internetPasswords = NULL; 110 NSDictionary* internetPasswordsQuery = @{ (id)kSecClass : (id)kSecClassInternetPassword, 111 (id)kSecReturnPersistentRef : @(YES), 112 (id)kSecUseDataProtectionKeychain : @(YES), 113 (id)kSecMatchLimit : (id)kSecMatchLimitAll }; 114 status = SecItemCopyMatching((__bridge CFDictionaryRef)internetPasswordsQuery, &internetPasswords); 115 CFErrorRef internetPasswordError = NULL; 116 if (status != errSecItemNotFound) { 117 SecError(status, &internetPasswordError, CFSTR("internet password query failed")); 118 if (internetPasswordError) { 119 [underlyingErrors addObject:CFBridgingRelease(internetPasswordError)]; 120 } 121 } 122 123 CFTypeRef keys = NULL; 124 NSDictionary* keysQuery = @{ (id)kSecClass : (id)kSecClassKey, 125 (id)kSecReturnPersistentRef : @(YES), 126 (id)kSecUseDataProtectionKeychain : @(YES), 127 (id)kSecMatchLimit : (id)kSecMatchLimitAll }; 128 status = SecItemCopyMatching((__bridge CFDictionaryRef)keysQuery, &keys); 129 CFErrorRef keyError = NULL; 130 if (status != errSecItemNotFound) { 131 if (keyError) { 132 [underlyingErrors addObject:CFBridgingRelease(keyError)]; 133 } 134 } 135 136 CFTypeRef certificates = NULL; 137 NSDictionary* certificateQuery = @{ (id)kSecClass : (id)kSecClassCertificate, 138 (id)kSecReturnPersistentRef : @(YES), 139 (id)kSecUseDataProtectionKeychain : @(YES), 140 (id)kSecMatchLimit : (id)kSecMatchLimitAll }; 141 status = SecItemCopyMatching((__bridge CFDictionaryRef)certificateQuery, &certificates); 142 CFErrorRef certificateError = NULL; 143 if (status != errSecItemNotFound) { 144 SecError(status, &certificateError, CFSTR("certificate query failed")); 145 if (certificateError) { 146 [underlyingErrors addObject:CFBridgingRelease(certificateError)]; 147 } 148 } 149 150 void (^scanArrayForCorruptedItem)(CFTypeRef, NSString*) = ^(CFTypeRef items, NSString* class) { 151 if ([(__bridge NSArray*)items isKindOfClass:[NSArray class]]) { 152 NSLog(@"scanning %d %@", (int)CFArrayGetCount(items), class); 153 for (NSData* persistentRef in (__bridge NSArray*)items) { 154 NSDictionary* itemQuery = @{ (id)kSecClass : class, 155 (id)kSecValuePersistentRef : persistentRef, 156 (id)kSecReturnAttributes : @(YES), 157 (id)kSecUseDataProtectionKeychain : @(YES) }; 158 CFTypeRef itemAttributes = NULL; 159 OSStatus copyStatus = SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &itemAttributes); 160 if (copyStatus != errSecSuccess && status != errSecInteractionNotAllowed) { 161 [corruptedItems addObject:itemQuery]; 162 } 163 } 164 } 165 }; 166 167 scanArrayForCorruptedItem(genericPasswords, (id)kSecClassGenericPassword); 168 scanArrayForCorruptedItem(internetPasswords, (id)kSecClassInternetPassword); 169 scanArrayForCorruptedItem(keys, (id)kSecClassKey); 170 scanArrayForCorruptedItem(certificates, (id)kSecClassCertificate); 171 172 if (underlyingErrors.count > 0 && error) { 173 *error = [NSError errorWithDomain:@"com.apple.security.keychainhealth" code:1 userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"encountered %d errors searching for corrupted items", (int)underlyingErrors.count], NSUnderlyingErrorKey : underlyingErrors.firstObject, @"searchingErrorCount" : @(underlyingErrors.count) }]; 174 } 175 176 return corruptedItems; 177 } 178 179 - (bool)deleteCorruptedItemsWithError:(NSError**)error 180 { 181 NSError* findError = nil; 182 NSArray* corruptedItems = [self findCorruptedItemsWithError:&findError]; 183 bool success = findError == nil; 184 185 NSMutableArray* deleteErrors = [[NSMutableArray alloc] init]; 186 for (NSDictionary* corruptedItem in corruptedItems) { 187 OSStatus status = SecItemDelete((__bridge CFDictionaryRef)corruptedItem); 188 if (status != errSecSuccess) { 189 success = false; 190 CFErrorRef deleteError = NULL; 191 SecError(status, &deleteError, CFSTR("failed to delete corrupted item")); 192 [deleteErrors addObject:CFBridgingRelease(deleteError)]; 193 } 194 } 195 196 if (error && (findError || deleteErrors.count > 0)) { 197 *error = [NSError errorWithDomain:@"com.apple.security.keychainhealth" code:2 userInfo:@{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"encountered %@ errors searching for corrupted items and %d errors attempting to delete corrupted items", findError.userInfo[@"searchingErrorCount"], (int)deleteErrors.count]}]; 198 } 199 200 return success; 201 } 202 203 - (void)rpcFindCorruptedItemsWithReply:(void (^)(NSArray* corruptedItems, NSError* error))reply 204 { 205 NSError* error = nil; 206 NSArray* corruptedItems = [self findCorruptedItemsWithError:&error]; 207 reply(corruptedItems, error); 208 } 209 210 - (void)rpcDeleteCorruptedItemsWithReply:(void (^)(bool success, NSError* error))reply 211 { 212 NSError* error = nil; 213 bool success = [self deleteCorruptedItemsWithError:&error]; 214 reply(success, error); 215 } 216 217 @end