/ keychain / KeychainStasher / KeychainStasher.m
KeychainStasher.m
  1  #import <xpc/private.h>
  2  
  3  #import "utilities/debugging.h"
  4  #import <Security/SecItemPriv.h>
  5  #import "LocalKeychainAnalytics.h"
  6  
  7  #import "KeychainStasher.h"
  8  
  9  NSString* const kApplicationIdentifier = @"com.apple.security.KeychainStasher";
 10  
 11  @implementation KeychainStasher
 12  
 13  - (NSError*)errorWithStatus:(OSStatus)status message:(NSString*)format, ... NS_FORMAT_FUNCTION(2, 3)
 14  {
 15      if (status == errSecSuccess) {
 16          return nil;
 17      }
 18  
 19      NSString* desc;
 20      if (format) {
 21          va_list ap;
 22          va_start(ap, format);
 23          desc = [[NSString alloc] initWithFormat:format arguments:ap];
 24          va_end(ap);
 25      }
 26      return [NSError errorWithDomain:NSOSStatusErrorDomain
 27                                 code:status
 28                             userInfo:desc ? @{NSLocalizedDescriptionKey : desc} : nil];
 29  }
 30  
 31  - (NSMutableDictionary*)baseQuery {
 32      return [@{
 33          (id)kSecUseDataProtectionKeychain : @YES,
 34          (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
 35          // Prevents backups in case this ever comes to the Mac, prevents sync
 36          (id)kSecAttrSysBound : @(kSecSecAttrSysBoundPreserveDuringRestore),
 37          // Belt-and-suspenders prohibition on syncing
 38          (id)kSecAttrSynchronizable : @NO,
 39          (id)kSecClass : (id)kSecClassKey,
 40          (id)kSecAttrApplicationLabel : @"loginstash",
 41          // This is the default, but just making sure
 42          (id)kSecAttrAccessGroup : kApplicationIdentifier,
 43      } mutableCopy];
 44  }
 45  
 46  - (void)stashKey:(NSData*)key withReply:(void (^)(NSError*))reply {
 47      secnotice("stashkey", "Will attempt to stash key");
 48      if (!key || key.length == 0) {
 49          reply([self errorWithStatus:errSecParam message:@"nil or empty key passed"]);
 50          return;
 51      }
 52  
 53      NSMutableDictionary* query = [self baseQuery];
 54      query[(id)kSecValueData] = key;
 55  
 56      OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
 57      secnotice("stashkey", "SecItemAdd result: %ld", (long)status);
 58  
 59      if (status == errSecDuplicateItem) {
 60          [[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStash hardFailure:NO result:[self errorWithStatus:status message:nil]];
 61          query[(id)kSecValueData] = nil;
 62          NSDictionary* update = @{(id)kSecValueData : key};
 63          status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
 64          secnotice("stashkey", "SecItemUpdate result: %ld", (long)status);
 65      }
 66  
 67      [[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStash hardFailure:YES result:[self errorWithStatus:status message:nil]];
 68      if (status == errSecSuccess) {
 69          reply(nil);
 70      } else {
 71          reply([self errorWithStatus:status message:@"Stash failed with keychain error"]);
 72      }
 73  }
 74  
 75  - (void)loadKeyWithReply:(void (^)(NSData*, NSError*))reply {
 76      secnotice("KeychainStasher", "Will attempt to retrieve stashed key");
 77  
 78      NSMutableDictionary* query = [self baseQuery];
 79      query[(id)kSecReturnData] = @YES;
 80  
 81      CFTypeRef object = NULL;
 82      OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &object);
 83  
 84      if (status != errSecSuccess) {
 85          if (status == errSecItemNotFound) {
 86              reply(nil, [self errorWithStatus:status message:@"No stashed key found"]);
 87          } else {
 88              reply(nil, [self errorWithStatus:status message:@"Keychain error"]);
 89          }
 90          [[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad hardFailure:YES result:[self errorWithStatus:status message:nil]];
 91          return;
 92      }
 93  
 94      NSData* key;
 95      if (!object || CFGetTypeID(object) != CFDataGetTypeID() || !(key = CFBridgingRelease(object))) {
 96          reply(nil, [self errorWithStatus:errSecInternalError
 97                                   message:@"No or bad object: %d / %lu / %d",
 98                                           object != NULL, object ? CFGetTypeID(object) : 0, key != nil]);
 99          [[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad
100                                                 hardFailure:YES
101                                                      result:[self errorWithStatus:errSecInternalError message:nil]];
102          return;
103      }
104  
105      // Caller does not need to wait for our delete
106      reply(key, nil);
107  
108      query[(id)kSecReturnData] = nil;
109      status = SecItemDelete((__bridge CFDictionaryRef)query);
110      if (status != errSecSuccess) {
111          seccritical("Unable to delete masterkey after load: %d", (int)status);
112      }
113      [[LocalKeychainAnalytics logger] logResultForEvent:LKAEventStashLoad hardFailure:NO result:[self errorWithStatus:status message:nil]];
114  }
115  
116  @end