SFKeychainControlManager.m   [plain text]


/*
 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#import "SFKeychainControlManager.h"
#import "SecCFError.h"
#import "builtin_commands.h"
#import "debugging.h"
#import <Security/SecItem.h>
#import <Security/SecItemPriv.h>
#import <Foundation/NSXPCConnection_Private.h>

NSString* kSecEntitlementKeychainControl = @"com.apple.private.keychain.keychaincontrol";

XPC_RETURNS_RETAINED xpc_endpoint_t SecServerCreateKeychainControlEndpoint(void)
{
    return [[SFKeychainControlManager sharedManager] xpcControlEndpoint];
}

@implementation SFKeychainControlManager {
    NSXPCListener* _listener;
}

+ (instancetype)sharedManager
{
    static SFKeychainControlManager* manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[SFKeychainControlManager alloc] _init];
    });

    return manager;
}

- (instancetype)_init
{
    if (self = [super init]) {
        _listener = [NSXPCListener anonymousListener];
        _listener.delegate = self;
        [_listener resume];
    }

    return self;
}

- (xpc_endpoint_t)xpcControlEndpoint
{
    return [_listener.endpoint _endpoint];
}

- (BOOL)listener:(NSXPCListener*)listener shouldAcceptNewConnection:(NSXPCConnection*)newConnection
{
    NSNumber* entitlementValue = [newConnection valueForEntitlement:kSecEntitlementKeychainControl];
    if (![entitlementValue isKindOfClass:[NSNumber class]] || !entitlementValue.boolValue) {
        secerror("SFKeychainControl: Client pid (%d) doesn't have entitlement: %@", newConnection.processIdentifier, kSecEntitlementKeychainControl);
        return NO;
    }

    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(SFKeychainControl)];
    [interface setClass:[NSError class] forSelector:@selector(rpcFindCorruptedItemsWithReply:) argumentIndex:1 ofReply:YES];
    [interface setClass:[NSError class] forSelector:@selector(rpcDeleteCorruptedItemsWithReply:) argumentIndex:1 ofReply:YES];
    newConnection.exportedInterface = interface;
    newConnection.exportedObject = self;
    [newConnection resume];
    return YES;
}

- (NSArray<NSDictionary*>*)findCorruptedItemsWithError:(NSError**)error
{
    NSMutableArray<NSDictionary*>* corruptedItems = [[NSMutableArray alloc] init];
    NSMutableArray* underlyingErrors = [[NSMutableArray alloc] init];

    CFTypeRef genericPasswords = NULL;
    NSDictionary* genericPasswordsQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
                                             (id)kSecReturnPersistentRef : @(YES),
                                             (id)kSecAttrNoLegacy : @(YES),
                                             (id)kSecMatchLimit : (id)kSecMatchLimitAll };
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordsQuery, &genericPasswords);
    CFErrorRef genericPasswordError = NULL;
    if (status != errSecItemNotFound) {
        SecError(status, &genericPasswordError, CFSTR("generic password query failed"));
        if (genericPasswordError) {
            [underlyingErrors addObject:CFBridgingRelease(genericPasswordError)];
        }
    }

    CFTypeRef internetPasswords = NULL;
    NSDictionary* internetPasswordsQuery = @{ (id)kSecClass : (id)kSecClassInternetPassword,
                                              (id)kSecReturnPersistentRef : @(YES),
                                              (id)kSecAttrNoLegacy : @(YES),
                                              (id)kSecMatchLimit : (id)kSecMatchLimitAll };
    status = SecItemCopyMatching((__bridge CFDictionaryRef)internetPasswordsQuery, &internetPasswords);
    CFErrorRef internetPasswordError = NULL;
    if (status != errSecItemNotFound) {
        SecError(status, &internetPasswordError, CFSTR("internet password query failed"));
        if (internetPasswordError) {
            [underlyingErrors addObject:CFBridgingRelease(internetPasswordError)];
        }
    }

    CFTypeRef keys = NULL;
    NSDictionary* keysQuery = @{ (id)kSecClass : (id)kSecClassKey,
                                 (id)kSecReturnPersistentRef : @(YES),
                                 (id)kSecAttrNoLegacy : @(YES),
                                 (id)kSecMatchLimit : (id)kSecMatchLimitAll };
    status = SecItemCopyMatching((__bridge CFDictionaryRef)keysQuery, &keys);
    CFErrorRef keyError = NULL;
    if (status != errSecItemNotFound) {
        if (keyError) {
            [underlyingErrors addObject:CFBridgingRelease(keyError)];
        }
    }

    CFTypeRef certificates = NULL;
    NSDictionary* certificateQuery = @{ (id)kSecClass : (id)kSecClassCertificate,
                                        (id)kSecReturnPersistentRef : @(YES),
                                        (id)kSecAttrNoLegacy : @(YES),
                                        (id)kSecMatchLimit : (id)kSecMatchLimitAll };
    status = SecItemCopyMatching((__bridge CFDictionaryRef)certificateQuery, &certificates);
    CFErrorRef certificateError = NULL;
    if (status != errSecItemNotFound) {
        SecError(status, &certificateError, CFSTR("certificate query failed"));
        if (certificateError) {
            [underlyingErrors addObject:CFBridgingRelease(certificateError)];
        }
    }

    void (^scanArrayForCorruptedItem)(CFTypeRef, NSString*) = ^(CFTypeRef items, NSString* class) {
        if ([(__bridge NSArray*)items isKindOfClass:[NSArray class]]) {
            NSLog(@"scanning %d %@", (int)CFArrayGetCount(items), class);
            for (NSData* persistentRef in (__bridge NSArray*)items) {
                NSDictionary* itemQuery = @{ (id)kSecClass : class,
                                             (id)kSecValuePersistentRef : persistentRef,
                                             (id)kSecReturnAttributes : @(YES),
                                             (id)kSecAttrNoLegacy : @(YES) };
                CFTypeRef itemAttributes = NULL;
                OSStatus copyStatus = SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &itemAttributes);
                if (copyStatus != errSecSuccess && status != errSecInteractionNotAllowed) {
                    [corruptedItems addObject:itemQuery];
                }
            }
        }
    };

    scanArrayForCorruptedItem(genericPasswords, (id)kSecClassGenericPassword);
    scanArrayForCorruptedItem(internetPasswords, (id)kSecClassInternetPassword);
    scanArrayForCorruptedItem(keys, (id)kSecClassKey);
    scanArrayForCorruptedItem(certificates, (id)kSecClassCertificate);

    if (underlyingErrors.count > 0 && error) {
        *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) }];
    }

    return corruptedItems;
}

- (bool)deleteCorruptedItemsWithError:(NSError**)error
{
    NSError* findError = nil;
    NSArray* corruptedItems = [self findCorruptedItemsWithError:&findError];
    bool success = findError == nil;

    NSMutableArray* deleteErrors = [[NSMutableArray alloc] init];
    for (NSDictionary* corruptedItem in corruptedItems) {
        OSStatus status = SecItemDelete((__bridge CFDictionaryRef)corruptedItem);
        if (status != errSecSuccess) {
            success = false;
            CFErrorRef deleteError = NULL;
            SecError(status, &deleteError, CFSTR("failed to delete corrupted item"));
            [deleteErrors addObject:CFBridgingRelease(deleteError)];
        }
    }

    if (error && (findError || deleteErrors.count > 0)) {
        *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]}];
    }

    return success;
}

- (void)rpcFindCorruptedItemsWithReply:(void (^)(NSArray* corruptedItems, NSError* error))reply
{
    NSError* error = nil;
    NSArray* corruptedItems = [self findCorruptedItemsWithError:&error];
    reply(corruptedItems, error);
}

- (void)rpcDeleteCorruptedItemsWithReply:(void (^)(bool success, NSError* error))reply
{
    NSError* error = nil;
    bool success = [self deleteCorruptedItemsWithError:&error];
    reply(success, error);
}

@end