SecItemAdd() 使用 kSecAttrAccessibleWhenUnlocked 成功但使用 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 失败

SecItemAdd() Succeeds with kSecAttrAccessibleWhenUnlocked But Fails with kSecAttrAccessibleWhenUnlockedThisDeviceOnly

到目前为止的故事

四个月前,我发布了 因为升级到 iOS 13 破坏了我的钥匙串相关代码。

我的代码使用 class kSecClassGenericPassword 和访问属性 kSecAttrAccessibleWhenUnlocked 将用户密码存储在钥匙串中。正如我自己对那个问题的回答所解释的那样,我最终通过稍微清理查询字典让我的代码也可以在 iOS 13 上工作。

目前的问题

几周前,我被要求禁用密码数据备份以增强安全性,因此我将访问属性更改为kSecAttrAccessibleWhenUnlockedThisDeviceOnly(与kSecAttrAccessibleWhenUnlocked,钥匙串中的密码在备份期间传输到另一台设备)。

现在,我的代码失败了,用户每次都必须输入密码。(在 iOS 13.0,iPhone 8 Plus 上测试)

当用户使用他们的密码登录时,我的代码首先使用 SecItemDelete() 删除任何先前存储的密码,然后使用 SecItemMatch().

继续存储输入的密码

由于将访问属性更改为 kSecAttrAccessibleWhenUnlockedThisDeviceOnlySecItemDelete() "succeeds" 和 errSecItemNotFound(即 "Nothing to delete"),但是 SecItemAdd() 失败 errSecDuplicateItem!

请注意,这不是尝试使用 kSecAttrAccessibleWhenUnlockedThisDeviceOnly 检索以前用 kSecAttrAccessibleWhenUnlocked 存储的密码的问题(即,用于存储和加载的不同查询字典);我从设备上删除了该应用程序并从头开始尝试使用新代码,但 SecItemAdd() 总是失败。

这是怎么回事?

我想你已经明白了,但为了完整起见:

Note, this isn't an issue of trying to retrieve a password previously stored with kSecAttrAccessibleWhenUnlocked using kSecAttrAccessibleWhenUnlockedThisDeviceOnly (i.e., different query dictionaries for store and load); I deleted the app from the device and tried from the start with the new code, and SecItemAdd() always fails.

这正是发生在你身上的事情。钥匙串内容在应用程序删除和 re-install 后仍然存在。要解决此问题,您需要提出一种迁移策略,该策略将删除使用相同主键保存的项目,但在尝试保存新项目之前使用不同的访问控制设置

如果您使用已弃用的可访问密钥,例如:“kSecAttrAccessibleAlwaysThisDeviceOnly”或 'kSecAttrAccessibleAlways',那么处理钥匙串中保存的项目的最佳方法是制定迁移策略,将使用这些已弃用密钥保存的钥匙串项目隐藏到 convert/save/copy 使用新密钥(如“kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly”。

没有更好的方法,因为钥匙串项目总是在应用程序删除和重新安装后仍然存在。

这是有效的代码: 注意:应该为所有键触发(对于每个 key/value 对)

+ (void)updateNewchainDataForKey:(NSString*)key {
    
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];

//Our OLD keychain items were saved with 'kSecAttrAccessibleAlwaysThisDeviceOnly'(DEPRECATED) accessible key
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];

[dict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];


CFDictionaryRef resultDataRef = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict, (CFTypeRef *)&resultDataRef);
NSDictionary *resultDict = (__bridge_transfer NSDictionary *)resultDataRef;

NSLog(@"resultDict %@", resultDict);

if( status != errSecSuccess) {
    NSLog(@"Unable to fetch item for key %@ with error:%d",key,(int)status);
    return;
}

if (status == errSecSuccess && resultDict) {
    
    // Check if we have the old attribute type(s)
    if ([[[resultDict objectForKey:(__bridge id)kSecAttrAccessible] copy] isEqualToString:(__bridge NSString *)(kSecAttrAccessibleAlways)]
        || [[[resultDict objectForKey:(__bridge id)kSecAttrAccessible] copy] isEqualToString:(__bridge NSString *)(kSecAttrAccessibleAlwaysThisDeviceOnly)]) {
        
        // Update the deviceID attribute to kSecAttrAccessibleAlwaysThisDeviceOnly
        NSMutableDictionary *updateQuery = [NSMutableDictionary dictionary];
        
        //Our keychain items are now being saved with 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly' accessible key
        // Set the new attribute
        [updateQuery setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
        
        NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
        [dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        
        NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
        [dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
        [dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
        
        // Perform the update
        OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)dict, (__bridge CFDictionaryRef)updateQuery);
        if (status != errSecSuccess) {
            NSLog(@"status failed %d", (int)status);
        } else {
            NSLog(@"status PASS %d", (int)status);
        }
        
    }
    
  }
}

注意:执行上述代码后,与old/deprecated可访问密钥一起保存的key/value对将被删除,只有key/value 用新的可访问密钥更新的对将存在于钥匙串中

希望,这有助于挽救某人的一天!