KeychainStasher.m   [plain text]

#import <xpc/private.h>

#import "utilities/debugging.h"
#import <Security/SecItemPriv.h>
#import "LocalKeychainAnalytics.h"

#import "KeychainStasher.h"

NSString* const kApplicationIdentifier = @"com.apple.security.KeychainStasher";

@implementation KeychainStasher

- (NSError*)errorWithStatus:(OSStatus)status message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3)
{
if (status == errSecSuccess) {
return nil;
}

NSString* desc;
if (format) {
va_list ap;
va_start(ap, format);
desc = [[NSString alloc] initWithFormat:format arguments:ap];
va_end(ap);
}
return [NSError errorWithDomain:NSOSStatusErrorDomain
code:status
userInfo:desc ? @{NSLocalizedDescriptionKey : desc} : nil];
}

- (NSMutableDictionary*)baseQuery {
return [@{
(id)kSecUseDataProtectionKeychain : @YES,
(id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
// Prevents backups in case this ever comes to the Mac, prevents sync
(id)kSecAttrSysBound : @(kSecSecAttrSysBoundPreserveDuringRestore),
// Belt-and-suspenders prohibition on syncing
(id)kSecAttrSynchronizable : @NO,
(id)kSecClass : (id)kSecClassKey,
(id)kSecAttrApplicationLabel : @"loginstash",
// This is the default, but just making sure
(id)kSecAttrAccessGroup : kApplicationIdentifier,
} mutableCopy];
}

- (void)stashKey:(NSData*)key withReply:(void (^)(NSError*))reply {
secnotice("stashkey", "Will attempt to stash key");
if (!key || key.length == 0) {
reply([self errorWithStatus:errSecParam message:@"nil or empty key passed"]);
return;
}

NSMutableDictionary* query = [self baseQuery];
query[(id)kSecValueData] = key;

OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
secnotice("stashkey", "SecItemAdd result: %ld", (long)status);

if (status == errSecDuplicateItem) {
[[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStash hardFailure:NO result:[self errorWithStatus:status message:nil]];
query[(id)kSecValueData] = nil;
NSDictionary* update = @{(id)kSecValueData : key};
status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
secnotice("stashkey", "SecItemUpdate result: %ld", (long)status);
}

[[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStash hardFailure:YES result:[self errorWithStatus:status message:nil]];
if (status == errSecSuccess) {
reply(nil);
} else {
reply([self errorWithStatus:status message:@"Stash failed with keychain error"]);
}
}

- (void)loadKeyWithReply:(void (^)(NSData*, NSError*))reply {
secnotice("KeychainStasher", "Will attempt to retrieve stashed key");

NSMutableDictionary* query = [self baseQuery];
query[(id)kSecReturnData] = @YES;

CFTypeRef object = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &object);

if (status != errSecSuccess) {
if (status == errSecItemNotFound) {
reply(nil, [self errorWithStatus:status message:@"No stashed key found"]);
} else {
reply(nil, [self errorWithStatus:status message:@"Keychain error"]);
}
[[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad hardFailure:YES result:[self errorWithStatus:status message:nil]];
return;
}

NSData* key;
if (!object || CFGetTypeID(object) != CFDataGetTypeID() || !(key = CFBridgingRelease(object))) {
reply(nil, [self errorWithStatus:errSecInternalError
message:@"No or bad object: %d / %lu / %d",
object != NULL, object ? CFGetTypeID(object) : 0, key != nil]);
[[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad
hardFailure:YES
result:[self errorWithStatus:errSecInternalError message:nil]];
return;
}

// Caller does not need to wait for our delete
reply(key, nil);

query[(id)kSecReturnData] = nil;
status = SecItemDelete((__bridge CFDictionaryRef)query);
if (status != errSecSuccess) {
seccritical("Unable to delete masterkey after load: %d", (int)status);
}
[[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad hardFailure:NO result:[self errorWithStatus:status message:nil]];
}

@end