ios 5.0 钥匙串访问

Posted

技术标签:

【中文标题】ios 5.0 钥匙串访问【英文标题】:ios 5.0 keychain access 【发布时间】:2011-10-29 22:03:08 【问题描述】:

我想将用户凭据存储在钥匙串中。我找到了这个: http://developer.apple.com/library/ios/#samplecode/GenericKeychain/Listings/Classes_KeychainItemWrapper_m.html#//apple_ref/doc/uid/DTS40007797-Classes_KeychainItemWrapper_m-DontLinkElementID_10

我将 KeychainItemWrapper.h/.m 添加到我的项目中。不幸的是它没有编译。我的目标是 iOS 5,我猜这就是问题所在。

例如,这一行:

    [genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];

给我这个错误:

将 C 指针类型“CFTypeRef”(又名“const void *”)转换为 Objective-C 指针类型“id”需要桥接转换

我尝试了“修复它”,但它只是引入了不同的错误。

关于如何进行的建议?我觉得奇怪的是,这个包装器一开始就没有内置到 SDK 中。 iOS 5 是否有新的 API/示例?我找不到一个。 iOS 5 真的还在保密协议下吗?

【问题讨论】:

如果您可以将已接受的答案从明显不正确的答案更改为对每个人都有帮助。 ARC Version of this code here 有一个要点。 【参考方案1】:

关闭 ARC 是一个短视的答案。我在下面包含了一个与 ARC 兼容的 KeychainWrapper 版本。

我从this project 得到它。

注意:这方面的专家(见下面的 cmets)认为这是一个更好的实现: https://gist.github.com/1170641

另外,请注意 KeyChain 凭据在您的应用被删除后仍然存在。如果您将其用于令牌身份验证,则可以考虑使用 NSUserDefaults。请参阅 this post 了解更多信息。

//File: KeychainWrapper.h
#import <UIKit/UIKit.h>

@interface KeychainWrapper : 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

和实施:

//File: KeychainWrapper.m
#import "KeychainWrapper.h"
#import <Security/Security.h>


static NSString *KeychainWrapperErrorDomain = @"KeychainWrapperErrorDomain";

#if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
@interface KeychainWrapper (PrivateMethods)
+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
@end
#endif

@implementation KeychainWrapper

#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: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        return nil;
    

    SecKeychainItemRef item = [KeychainWrapper 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        return;
    

    OSStatus status = noErr;

    SecKeychainItemRef item = [KeychainWrapper getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];

    if (*error && [*error code] != noErr) 
        return;
    

    *error = nil;

    // null means it's in the default keychain
    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: KeychainWrapperErrorDomain code: status userInfo: nil];
    


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

    *error = nil;

    SecKeychainItemRef item = [KeychainWrapper 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: KeychainWrapperErrorDomain code: status userInfo: nil];
    


+ (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error 
    if (!username || !serviceName) 
        *error = [NSError errorWithDomain: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain code: -1999 userInfo: nil];
            
        
        else 
            // Something else went wrong. Simply return the normal Keychain API error code.
            if (error != nil) 
                *error = [NSError errorWithDomain: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain code: -2000 userInfo: nil];
        
        return NO;
    

    // See if we already have a password entered for these credentials.
    NSError *getError = nil;
    NSString *existingPassword = [KeychainWrapper 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain 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: KeychainWrapperErrorDomain code: status userInfo: nil];      

        return NO;
    

    return YES;


#endif

@end

【讨论】:

在 UserDefault 中保存凭据?多么糟糕的建议,始终使用 KeyChain! 我认为在 UserDefaults 中存储一个临时令牌很好。当然不是用户名和密码。 请勿将其视为个人,但请不要使用此代码 sn-p。 __bridge_transfer 将减少 CFRefs 上的保留计数。您正在系统定义的常量上使用它,这可能会导致麻烦。我对关闭 ARC 是短视的说法也有很大的问题。选择性禁用 ARC 的主要原因之一是因为您进行了过多的 NS CF 转换。在使用钥匙串时,如果您使用未经审查的代码,您不仅可以解决内存泄漏、内存崩溃问题,而且还有潜在的安全问题。 @amatn 对不起。你对iOS平台的理解显然比我高。您将如何制作与 ARC 兼容的 KeychainWrapper 版本? @Flaviu 经过简短的代码审查后,Ahmed Khalaf 链接到的代码看起来正确实现:gist.github.com/1170641【参考方案2】:

这是为我成功编译的ARCified version。只要记住link 安全框架与您的目标。

希望这会有所帮助!

【讨论】:

【参考方案3】:

任何对 CF 类的引用都必须与“__bridge”语句配对才能在 Objective-C 和核心基础类之间进行转换

试试这个:

[genericPasswordQuery setObject:identifier forKey:(__bridge id) kSecAttrGeneric];

【讨论】:

以这种方式而不是@Flaviu建议的方式这样做有什么缺点吗?【参考方案4】:

将您的代码更改为以下代码:

[genericPasswordQuery setObject:identifier forKey:(__bridge id)kSecAttrGeneric];

来源:https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/iPhoneTasks/iPhoneTasks.html

【讨论】:

【参考方案5】:

您的问题不是 iOS 5,而是 ARC。我建议关闭这些文件甚至整个项目的 ARC。

【讨论】:

+3/-17 仍被选为答案...即使我无法标记它,“不应标记技术上不正确的答案” 我没有编辑它只是因为我现在觉得它很搞笑。检查日期人们,当时不值得转换为 ARC。 我敢肯定,即使您使用 +3/-18 也不会感觉良好。您可以更新答案,或者要求删除。这是我个人的看法。

以上是关于ios 5.0 钥匙串访问的主要内容,如果未能解决你的问题,请参考以下文章

Mac钥匙串有啥用?钥匙串访问是啥

使用钥匙串提高 iOS 应用程序的安全性

即使在从钥匙串访问和 App Store Connect 中删除后,重新启动 Xcode 时,已删除的 iOS 证书仍会继续显示在钥匙串中

“访问钥匙串时出错”

将钥匙串共享添加到已有用户的生产应用程序

iOS 钥匙串安全性