使用iPhone SDK保存在钥匙串中时出现错误

30
我使用苹果的钥匙串包装器来保存项目(在模拟器上运行,iOS 4.1)。

我以前没有使用过钥匙串。

我收到了以下错误信息:

无法添加钥匙串项。错误-25299

在KeychainItemWrapper.m的304行:

// No previous item found; add the new one.
result = SecItemAdd((CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
NSAssert( result == noErr, @"Couldn't add the Keychain Item." );

这是我保存的方法:
- (void) saveKey:(NSString *)key value:(NSString *)value {
    KeychainItemWrapper *keyItem = [[KeychainItemWrapper alloc] initWithIdentifier:key accessGroup:nil];
    [keyItem setObject:value forKey:(id)kSecValueData];
    [keyItem release];
}

以下是API尝试保存的值:

<CFBasicHash 0x7231f60 [0x320d380]>{type = mutable dict, count = 5,
entries =>
2 : <CFString 0x2e6eb98 [0x320d380]>{contents = "labl"} = <CFString 0x2fb018 [0x320d380]>{contents = ""}
3 : <CFString 0x2e6efb8 [0x320d380]>{contents = "v_Data"} = <CFString 0x727de60 [0x320d380]>{contents = "dit8"}
4 : <CFString 0x2e6ebc8 [0x320d380]>{contents = "acct"} = <CFString 0x2fb018 [0x320d380]>{contents = ""}
5 : <CFString 0x2e6eb58 [0x320d380]>{contents = "desc"} = <CFString 0x2fb018 [0x320d380]>{contents = ""}
6 : <CFString 0x2e6ebe8 [0x320d380]>{contents = "gena"} = <CFString 0x2ffd08 [0x320d380]>{contents = "userCode"}
}

我卡在这里了,所以我觉得悬赏是个好主意。同样的错误代码但 iOS4.3。但我猜这是 PEBKAC 问题,而不是 SDK 的问题。 - Matthias Bauch
这个问题发生在我向- (void)resetKeychainItem方法中添加了[keychainItemData setObject:@"" forKey:(__bridge id)kSecAttrService];时。当我读到下面user379075的回答时,我恍然大悟:如果你“重置”它,你也需要进行设置,反之亦然。 - mrd3650
7个回答

62

我知道这个问题已经过去几个月了,但是我刚刚遇到了同样的问题,并且解决这个问题非常痛苦,所以我想分享一下。我通过添加以下代码来解决问题:

[self.keychainItemWrapper setObject:@"MY_APP_CREDENTIALS" forKey:(id)kSecAttrService];
//@"MY_APP_CREDENTIALS" can be any string.

我发现这篇博客非常有帮助:
“在数据库术语中,你可以想象在kSecAttrAccount和kSecAttrService两个属性上有一个唯一索引,要求这两个属性的组合对于密钥链中的每个条目都是唯一的。”(来自http://useyourloaf.com/blog/2010/4/28/keychain-duplicate-item-when-adding-password.html)。

此外,在苹果公司使用此代码的示例项目中,他们在应用程序委托中实例化了KeychainItemWrapper。我不知道是否必要,但我喜欢尽可能地遵循他们的示例:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
//there will be some standard code here.
KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"MY_APP_CREDENTIALS" accessGroup:nil];
self.keychainWrapper = wrapper;
[self.keychainWrapper setObject:@"MYOBJECT" forKey:(id)kSecAttrService];
[wrapper release];
}

我认为这是包装器代码中的一个错误。逻辑基本上是这样说的:“这个条目已经存在了吗?没有,它不存在。好的,我会添加它。哎呀,你不能添加它,因为它已经存在了。”

你可能还需要设置kSecAttrAccount;我从未尝试过不设置此值,因为它旨在保存与密码相关联的用户名:

[self.wrapper setObject:txtUserName.text forKey:(id)kSecAttrAccount];   

添加了MY_APP_CREDENTIALS语句后,一切都正常工作了。非常感谢 :) - ilight
1
需要注意的是,当查找钥匙串项目时,您应该仅提供帐户和服务属性进行搜索。如果提供了更多属性,则系统将在所有属性上搜索匹配项,因此即使已存在该帐户和服务的项目,您也可能无法获得任何返回结果,因此尝试将具有该帐户和服务的条目添加到钥匙串中将失败。 - jpb

9
根据文档,您遇到的-25299错误是“errSecDuplicateItem”,这意味着您要添加的项目已经存在。查看KeychainItemWrapper的链接代码,我猜测SecItemCopyMatching调用失败,并出现错误而非errSecItemNotFound(-25300)。

6

使用Buzz Andersen开发的SFHFKeychainUtils,您可以轻松地使用钥匙串存储和检索值。

  1. 下载并将SFHFKeychainUtils.h和.m文件复制到项目中
  2. 将Security.framework添加到您的Framework文件夹中
  3. 确保这些文件已被添加到您的目标中
  4. 在要使用它的地方导入SFHFKeychainUtils.h

这是一个使用此库的小例子。

// To store data
NSError *error = nil;
[SFHFKeychainUtils storeUsername:username andPassword:password forServiceName:kStoredData updateExisting:YES error:&error];

// To retrieve data
NSString *password = [SFHFKeychainUtils getPasswordForUsername:username andServiceName:kStoredData error:&error];

// To delete data from keychain
[SFHFKeychainUtils deleteItemForUsername:username andServiceName:kStoredData error:&error];

在这个上下文中,kStoredData是什么?当我尝试这样做时,我遇到了构建错误。 - Philip Walton
在这种情况下,kStoredData是一个定义好的键...您可以使用任何键...在这个例子中,kStoreData是一个服务名称的键,例如@"com.company.app.serviceName"。您可以在.h文件中定义一个类似的键,然后使用它来存储和检索数据。 - matteodv

3

钥匙串是个大麻烦。你应该使用Buzz Andersen的STUtils库,作为一个封装器。它将让你的生活变得更加轻松。我从未遇到过问题。


2
这个 Github 项目现已被弃用;作者建议使用 https://github.com/ldandersen/STUtils/blob/master/Security 作为替代。 - Graham Miln

1

我也遇到了这个问题,感谢接受者的答案和额外链接的帮助,我解决了这个问题。

我的问题很有趣,我只需要保存一个值,并决定将其存储在字段kSecValueData中。那是因为我看到其他关于使用钥匙串的帖子并开始了自己的实现,然后才转向KeychainItemWrapper。 这导致了以下问题:在我测试的第一台设备(iPad 1st gen)上,我在writeToKeychain中遇到了错误。我更换了设备(也是iPad 1st gen),它可以工作!回到第一台设备时,它仍然无法工作。

所以在那时,我知道我之前在设备的钥匙串中做错了什么,并且不能轻易地恢复它。我得到的错误代码是:在writeToKeychain的SecItemCopyMatching(未找到项目)上是-25300,紧接着是在SecItemAdd上是-25299。(项目重复)

通过这个问题,这一切都有意义了:设备有一个与任何新密钥匹配的密钥,但KeychainItemWrapper无法删除它,但无法检索该密钥。 只要我将相同的值添加到kSecAttrAccount字段中,它就开始工作了。

长话短说,对于其他用户遇到这个问题,你的问题可能看起来不同,但要注意细节。如果你遇到了-25300(未找到项目)后跟着-25299(项目重复);请确保你设置了一个定义密钥链项目唯一性的字段。如果在一个设备上无法工作,请尝试另一个设备,如果可以的话,你可能能够将问题隔离到一个设备上。 苹果密钥链错误代码:http://developer.apple.com/library/ios/#documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/uid/TP30000898-CH5g-CJBEABHG(搜索结果代码)。

我如何找到我之前保存的项目?我以这种方式枚举了所有项目https://dev59.com/l2gu5IYBdhLWcg3w8Lav,但没有找到。它在哪里隐藏着? - below

1
我尝试了上面提到的所有解决方案,但都没有对我起作用。它只在实际设备上运行,而不在模拟器上运行。
我在模拟器上运行它的解决方案是打开“共享钥匙串授权”。 共享钥匙串授权

1
对我来说,解决方案是创建一个 KeychainItemWrapper "单例" 并在整个应用程序中使用它。(实际上,在我的情况下,我有一个装满了 KeychainItemWrapper 的单例字典,因为我使用了多个。)
这解决了我遇到的问题,即到达一个代码路径,这条路径实际上是“这个项在钥匙串中存在吗?不存在?那么就添加它。哎呀!尝试添加一个已经存在的项(错误 -25299)”
虽然我不确定,但我认为问题与钥匙串同步有关。我在使用NSUserDefaults时也遇到了类似的问题,当我写入 NSUD,然后在代码的其他地方获取 standardUserDefaults 并从中读取时,更新尚未发生(因为我还没有执行 [ud synchronize])。
在代码中,我的例程看起来像这样:
+ (KeychainItemWrapper*) keyChainWrapperForKeyID: (NSString*) keyID
{
    static dispatch_once_t onceToken = 0;
    static NSMutableDictionary *rfcuKeyChains = nil;
    dispatch_once(&onceToken, ^{
        rfcuKeyChains = [NSMutableDictionary new];
    });

    KeychainItemWrapper *keychain = nil;
    @synchronized (rfcuKeyChains)
    {
        keychain = [rfcuKeyChains objectForKey: keyID];
        if (keychain == nil)
        {
            keychain = [[KeychainItemWrapper alloc] initWithIdentifier: keyID accessGroup: nil];
            [rfcuKeyChains setObject: keychain forKey: keyID];
        }
    }

    return keychain;
}

我这样使用它:
KeychainItemWrapper *keychain = [RFCUtils keyChainWrapperForKeyID: keyID];
NSString *firstLaunch = [keychain objectForKey: (__bridge id)(kSecAttrAccount)];
if (firstLaunch == nil)
{
    [keychain setObject: MY_APP_KEY forKey: (__bridge id)(kSecAttrAccount)];
}

(等等,在这里)等其他地方也有类似的调用。

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