SFHFKeychainUtils. iOS钥匙串。支持ARC。

16
我想知道是否有人成功将SFHFKeychainUtils修改为兼容ARC。更确切地说是
NSDictionary *attributeResult = NULL;
NSMutableDictionary *attributeQuery = [query mutableCopy];
[attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge id) kSecReturnAttributes];
OSStatus status = SecItemCopyMatching((CFDictionaryRef) attributeQuery,(CFTypeRef *)(attributeResult));

我尝试过

OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) attributeQuery,(CFTypeRef *)(attributeResult));

同样也

CFTypeRef subAttributeResult = (CFTypeRef *)(objc_unretainedPointer(attributeResult));
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) attributeQuery,(CFTypeRef *)(subAttributeResult));
这是我所能做到的两种方法中唯一没有出现错误的两种方法。通过在这里和那里添加objc_XXX而不是CFTypeRef来添加任何其他方法都会导致错误(从obj-c指针到CFTypeRef的隐式转换在ARC中是不允许的,将x参数传递给y参数会放弃限定符)。显然,第一个代码片段也会报错。虽然编译时没有错误,但当到达此代码部分时,应用程序会崩溃,并出现EXC_BAD_ACCESS。 完整SFHFKeychainUtils的链接:https://github.com/ldandersen/scifihifi-iphone/tree/master/security 请帮忙,谢谢。
7个回答

22
这里是兼容ARC的SFHFKeychainUtils,
SFHFKeychainUtils.h
//
//  SFHFKeychainUtils.h
//
//  Created by Buzz Andersen on 10/20/08.
//  Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.
//  Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.
//
//  Permission is hereby granted, free of charge, to any person
//  obtaining a copy of this software and associated documentation
//  files (the "Software"), to deal in the Software without
//  restriction, including without limitation the rights to use,
//  copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the
//  Software is furnished to do so, subject to the following
//  conditions:
//
//  The above copyright notice and this permission notice shall be
//  included in all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//  OTHER DEALINGS IN THE SOFTWARE.
//

#import <UIKit/UIKit.h>


@interface SFHFKeychainUtils : NSObject {

}

+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;

@end

SFHFKeychainUtils.m

//  
//  SFHFKeychainUtils.m 
//

//  Created by Buzz Andersen on 10/20/08.   
//  Based partly on code by Jonathan Wight, Jon Crosby, and Mike Malone.    
//  Copyright 2008 Sci-Fi Hi-Fi. All rights reserved.   
//

//  Permission is hereby granted, free of charge, to any person 
//  obtaining a copy of this software and associated documentation  
//  files (the "Software"), to deal in the Software without 
//  restriction, including without limitation the rights to use,    
//  copy, modify, merge, publish, distribute, sublicense, and/or sell   
//  copies of the Software, and to permit persons to whom the   
//  Software is furnished to do so, subject to the following    
//  conditions: 
//

//  The above copyright notice and this permission notice shall be  
//  included in all copies or substantial portions of the Software. 
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
//  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND    
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
//  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,    
//  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR   
//  OTHER DEALINGS IN THE SOFTWARE. 
//

#import "SFHFKeychainUtils.h"   
#import <Security/Security.h>   


static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR

@interface SFHFKeychainUtils (PrivateMethods)   
(SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error; 
@end

#endif

@implementation SFHFKeychainUtils

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR

 +(NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {       
      if (!username || !serviceName) {          
           *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];          
            return nil;         
          }      

      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
      if (*error || !item) {            
            return nil;         
          }

      // from Advanced Mac OS X Programming, ch. 16     
      UInt32 length;        
      char *password;       
      SecKeychainAttribute attributes[8];       
      SecKeychainAttributeList list;
      attributes[0].tag = kSecAccountItemAttr;      
      attributes[1].tag = kSecDescriptionItemAttr;      
      attributes[2].tag = kSecLabelItemAttr;
      attributes[3].tag = kSecModDateItemAttr;
      list.count = 4;       
      list.attr = attributes;
     OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
      if (status != noErr) {            
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];            
            return nil;         
          }
     NSString *passwordString = nil;
     if (password != NULL) {        
           char passwordBuffer[1024];
            if (length > 1023) {                
                 length = 1023;             
                }           
            strncpy(passwordBuffer, password, length);
            passwordBuffer[length] = '\0';          
            passwordString = [NSString stringWithCString:passwordBuffer];           
          }
      SecKeychainItemFreeContent(&list, password);
      CFRelease(item);
      return passwordString;        
    }

+ (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {  

      if (!username || !password || !serviceName) {         
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];         
            return;         
          }
      OSStatus status = noErr;
      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
      if (*error && [*error code] != noErr) {           
            return;         
          }
      *error = nil;     

      if (item) {           
            status = SecKeychainItemModifyAttributesAndData(item,NULL,strlen([password UTF8String]),[password UTF8String]);
            CFRelease(item);            
          }     
      else {            
            status = SecKeychainAddGenericPassword(NULL,strlen([serviceName UTF8String]),[serviceName UTF8String],strlen([username UTF8String]),[username UTF8String],strlen([password UTF8String]),[password UTF8String],NULL);

          }
      if (status != noErr) {            
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];            
          }     
    }

+ (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {    
      if (!username || !serviceName) {          
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];          
            return;        
          }

      *error = nil;

      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
      if (*error && [*error code] != noErr) {        
          return;           
          }

      OSStatus status;    
      if (item) {        
           status = SecKeychainItemDelete(item);            
            CFRelease(item);        
          }

      if (status != noErr) {            
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];            
          }     
    }

+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {    
      if (!username || !serviceName) {          
            *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];        
            return nil;         
          }

      *error = nil;    
      SecKeychainItemRef item;   
      OSStatus status = SecKeychainFindGenericPassword(NULL,strlen([serviceName UTF8String]),[serviceName UTF8String],strlen([username UTF8String]),[username UTF8String], NULL,NULL,&item);

      if (status != noErr) {        
            if (status != errSecItemNotFound) {             
                  *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];              
                }        
            return nil;         
          }
          return item;    
    }

#else

+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {

      if (!username || !serviceName) {          
            if (error != nil) {             
                  *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];           
                }           
            return nil;        
          }      

      if (error != nil) {           
            *error = nil;           
          }   
      // Set up a query dictionary with the base query attributes: item type (generic), username, and service    
     NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];  
     NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil];    
     NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];    
      // First do a query for attributes, in case we already have a Keychain item with no password data set.    
      // One likely way such an incorrect item could have come about is due to the previous (incorrect)    
      // version of this code (which set the password as a generic attribute instead of password data).   
      NSMutableDictionary *attributeQuery = [query mutableCopy];   
      [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes];       
       CFTypeRef attrResult = NULL;  
      OSStatus status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) attributeQuery, &attrResult);       
      //NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult;   
      if (status != noErr) {        
            // No existing item found--simply return nil for the password           
           if (error != nil && status != errSecItemNotFound) {              
                  //Only return an error if a real exception happened--not simply for "not found."           
                  *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];              
                }    
            return nil;         
          }

     // We have an existing item, now query for the password data associated with it.    
      NSMutableDictionary *passwordQuery = [query mutableCopy];    
      [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData];     
        CFTypeRef resData = NULL;       
      status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData);      
      NSData *resultData = (__bridge_transfer NSData *)resData;    
      if (status != noErr) {       
            if (status == errSecItemNotFound) {            
                  // We found attributes for the item previously, but no password now, so return a special error.               
                  // Users of this API will probably want to detect this error and prompt the user to               
                  // re-enter their credentials.  When you attempt to store the re-entered credentials              
                  // using storeUsername:andPassword:forServiceName:updateExisting:error           
                  // the old, incorrect entry will be deleted and a new one with a properly encrypted               
                  // password will be added.

                  if (error != nil) {                   
                        *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];                 
                      }             
                }       
            else {              
                 // Something else went wrong. Simply return the normal Keychain API error code.            
                  if (error != nil) {                   
                        *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];                    
                     }              
                }           
           return nil;          
          }   
      NSString *password = nil;     
     if (resultData) {          
            password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];          
          } 
      else {        
            // There is an existing item, but we weren't able to get password data for it for some reason,          
            // Possibly as a result of an item being incorrectly entered by the previous code.          
            // Set the -1999 error so the code above us can prompt the user again.

           if (error != nil) {      
                  *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];               
                }           
          }     
      return password;      
    }

+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error 

{           
     if (!username || !password || !serviceName)

          {            
                if (error != nil)                   
                    {                       
                         *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];                    
                 }              
                return NO;          
              }

    // See if we already have a password entered for these credentials.

      NSError *getError = nil;      
      NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];        

     if ([getError code] == -1999)     
          {            
                // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.

                // Delete the existing item before moving on entering a correct one.            
               getError = nil;          

                [self deleteItemForUsername: username andServiceName: serviceName error: &getError];            

                if ([getError code] != noErr)                 
                   {                   
                          if (error != nil)                         
                              {                            
                                    *error = getError;                           
                                 }                      
                          return NO;                        
                       }            
              }     
    else if ([getError code] != noErr)      
          {           
                if (error != nil)                   
                    {                       
                          *error = getError;                    
                        }               
                return NO;          
              }     
     if (error != nil)          
          {          
                *error = nil;               
              }

      OSStatus status = noErr;

      if (existingPassword)         
          {

                // We have an existing, properly entered item with a password.        
                // Update the existing item.            

               if (![existingPassword isEqualToString:password] && updateExisting)                
                    {                       
                          //Only update if we're allowed to update existing.  If not, simply do nothing.

                          NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,kSecAttrService,kSecAttrLabel,kSecAttrAccount,nil];

                          NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,serviceName,serviceName,username,nil];

                          NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];      

                          status = SecItemUpdate((__bridge_retained CFDictionaryRef) query, (__bridge_retained CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]);                      
                        }               
              }     
      else      
          {             
               // No existing entry (or an existing, improperly entered, and therefore now

                // deleted, entry).  Create a new entry.


                NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,kSecAttrService,kSecAttrLabel,kSecAttrAccount,kSecValueData,nil];

                NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,serviceName,serviceName,username,[password dataUsingEncoding: NSUTF8StringEncoding],nil];

                NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];            

                status = SecItemAdd((__bridge_retained CFDictionaryRef) query, NULL);               
              }     
      if (error != nil && status != noErr)          
          {            
                // Something went wrong with adding the new item. Return the Keychain error code.               
                *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];                
                return NO;            
              }     
      return YES;       
    }

+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error    
{       
      if (!username || !serviceName)        
          {            
                if (error != nil)                   
                   {                        
                          *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];                       
                        }            
                return NO;          
              }     
      if (error != nil)       
          {             
               *error = nil;            
              }     
      NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];     
      NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];        
      NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];      
      OSStatus status = SecItemDelete((__bridge_retained CFDictionaryRef) query);       

      if (error != nil && status != noErr)        
          {           
                *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];            
                return NO;           
              }    
      return YES;    
    }
#endif
@end

7
这里所有的桥接操作都是完全错误的。例如,(__bridge_transfer NSString *) kSecClass 是错误的。__bridge_transfer 表示您拥有(即已保留)kSecClass,并将该所有权传递给 ARC。但是,您从未保留过 kSecClass,因此这是一个过度释放。另一个例子,(__bridge_retained CFDictionaryRef) passwordQuery 是错误的。__bridge_retained 转换会使你拥有所得到的 CF 对象,即它为你保留了它。然而,你只是将它传递给一个函数,你从未释放它。这是一个泄漏。 - user102008
@user102008 谢谢,非常简单明了的解释。 - Aragunz

6

我曾经遇到了同样的问题 - 不知道应该归功于谁 - 但是在这个 github 项目 中的 SFHFKeychainUtils 版本可以作为 ARC 的替代品使用。


4
如果有人误打误撞来到这个页面并有同样的问题,我建议你看一下Sam Soffes SSKeychain组件。它不仅与ARC兼容,而且文档齐全,维护得好,并且比SFHFKeychainUtils具有更多的功能。

非常感谢,Emmanuel。切换到SSKeychain消除了我在使用ARC版本的SFHFKeychainUtils时遇到的所有内存泄漏问题。 - lifjoy

1
你可以随时将那个文件设置为非ARC模式。

是的,我发帖后意识到了这一点,所以我进行了更正。但是知道解决方案会很好,至少可以作为将来(或其他类似问题)的参考。无论如何,感谢您的回答 ;) - Roland
ARC文档确实介绍了如何处理CFRefs,请参阅ARCProgrammingGuide.pdf。此外,Mike Ash最近在博客中发表了一篇很好的文章:Automatic Reference Counting - zaph
这是一篇不错的文章,我会将这个文档加入书签。问题是,它并没有真正帮助到我解决这个特定的问题,或者说我并没有真正理解如何去做。我尝试给 attributeResult 变量添加 __bridge、__bridge_transfer 或 __bridge_retain,就像我对 attributeQuery 变量所做的那样(应该从一开始就提到,给 attributeQuery 变量添加 __bridge 后不再出现错误),但它仍然会出现错误。声明看起来像是 "OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result)"。带指针的那个会出现错误,另一个则不会。 - Roland
抱歉,我不能提供更多帮助,因为我除了在小项目中使用过外,还没有使用过ARC和CF。 - zaph

0

我刚刚将另一个框架(MPOAuth)移植到ARC,这是我解决这个问题的方法:

NSString *foundPassword = nil;
NSData *foundValue = nil;
CFTypeRef foundDict;
status = SecItemCopyMatching((__bridge CFDictionaryRef)attributeQuery, &foundDict);

if (status == noErr) {
    attributesDictionary = (__bridge_transfer NSDictionary *)foundDict;
    foundValue = [attributesDictionary objectForKey:(__bridge id)kSecValueData];
    if (foundValue) {
        foundPassword = [[NSString alloc] initWithData:foundValue
                                              encoding:NSUTF8StringEncoding];
    }
}

我需要验证这是否会导致foundDict泄漏。


0

1
如果您查看STUtils的此实现,您会注意到它也不支持ARC。快速搜索“release”将向您展示这一点。 - Etienne
我认为提出这个问题很有用,因为可以制作一个兼容ARC的SFUtils版本;库的作者可能有其他原因来废弃SFHFKeychainUtils,而这是被接受答案所基于的。此外,OP可以简单地使用-fno-objc-arc标记此库,这比使用已弃用的库更好。 - Matthew Quiros
STKeychain还带了一堆无关的东西,所以重新有一个独立的实现会很好。 - Sofi Software LLC

0
请看这个问题:

iPhone 从钥匙串中获取数据字典

我还没有测试过,但听起来是正确的。您不需要禁用 ARC。它还有关于如何消除错误的说明。

在 ARC 下,将间接指针强制转换为 Objective-C 指针 'CFTypeRef*'(即 'const void **')被禁止。

的提示。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接