James Yu


写了那么多的if else 依然未能参透其精髓。。。


keychain(一)

iOS keychain 主要是用来保存一些用户敏感数据。比如用户密码,token。keychain是用SQLite进行存储的。用苹果的话来说是一个专业的数据库,加密我们保存的数据,可以通过metadata(attributes)进行高效的搜索。keychain适合保存一些比较小的数据量的数据,如果要保存大的数据,可以考虑文件的形式存储在磁盘上,在keychain里面保存解密这个文件的密钥。

keychain的类型

  • kSecClassGenericPassword
  • kSecClassInternetPassword
  • kSecClassCertificate
  • kSecClassKey
  • kSecClassIdentity

这5个类型只是对应于不同的item,存储的属性有区别,使用上都是一样的。

不同类型对应的属性:

kSecClassGenericPassword kSecClassInternetPassword kSecClassCertificate kSecClassKey kSecClassIdentity
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrService
kSecAttrGeneric
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrSecurityDomain
kSecAttrServer
kSecAttrProtocol
kSecAttrAuthenticationType
kSecAttrPort
kSecAttrPath
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCertificateType
kSecAttrCertificateEncoding
kSecAttrLabel
kSecAttrSubject
kSecAttrIssuer
kSecAttrSerialNumber
kSecAttrSubjectKeyID
kSecAttrPublicKeyHash
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrKeyClass
kSecAttrLabel
kSecAttrApplicationLabel
kSecAttrIsPermanent
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
kSecAttrCanEncrypt
kSecAttrCanDecrypt
kSecAttrCanDerive
kSecAttrCanSign
kSecAttrCanVerify
kSecAttrCanWrap
kSecAttrCanUnwrap
kSecAttrSynchronizable
kSecClassKey + kSecClassCertificate 属性

既然苹果是采用SQLite去存储的,那么以上这些不同item的attribute可以理解是数据库里面表的字段。那么对keychain的操作其实也就是普通数据库的增删改查了。这样也许就会觉得那些API也没那么难用了。

NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,  
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
   
    CFErrorRef error = NULL;
   
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

以这个添加kSecClassGenericPassword item为例,在字典里面我们设置了以下几个属性:获取权限为当设备处于未锁屏状态,item的类型为kSecClassGenericPassword,item的value为@"123456", item的账户名为@"account name", item的service为@"loginPassword"。最后,调用SecItemAdd进行插入。使用上有点像CoreData。

NSDictionary *query = @{  
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecAttrService : @"loginPassword",
                            (__bridge id)kSecAttrAccount : @"account name"
                            };

    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);

删除同样也是指定之前存的item的属性,最后调用SecItemDelete这个方法。这边要注意的是劲量用多个字段确定这个item,(虽然平常开发都可能是唯一)防止删除了其他item;比如我们把kSecAttrAccount这个属性去掉,那么将会删除所有的kSecAttrService对应value为@"loginPassword"的item;

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,  
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };
    NSDictionary *update = @{
                             (__bridge id)kSecValueData : [@"654321" dataUsingEncoding:NSUTF8StringEncoding],
                             };

    OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);

苹果推荐我们用SecItemUpdate去修改一个已经存在的item,可能我们喜欢先调用SecItemDelete方法去删除,再添加一个新的。这个主要目的是防止新添的item丢失了原来的部分属性。这个方法需要两个入参,一个字典是用来指定要更新的item,另一个字典是想要更新的某个属性的value,最后调用SecItemUpdate。

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,  
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"loginPassword",
                            };

    CFTypeRef dataTypeRef = NULL;

    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);

    if (status == errSecSuccess) {
        NSString *pwd = [[NSString alloc] initWithData:(__bridge NSData * _Nonnull)(dataTypeRef) encoding:NSUTF8StringEncoding];
        NSLog(@"==result:%@", pwd);
    }

查和前面几个操作类似,首先同样是指定属性定位到这个item,最后调用SecItemCopyMatching方法。既然是数据库查询,肯定会有记录的条数的问题。本例中使用了kSecMatchLimitOne,表示返回结果集的第一个,当然这个也是默认的。如果是查询出多个,kSecMatchLimitAll可以使用这个,那么返回的将是个数组。SecItemCopyMatching方法的入参dataTypeRef,是一个返回结果的引用,会根据不同的item,返回对应不同的类型(如NSCFData, NSCFDictionary, NSCFArray等等)。

刚刚上面是返回存储的value的引用,如果我们想看看这个item所有的属性怎么办?我们可以使用kSecReturnRef

 NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecReturnRef : @YES,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrService : @"noraml",
                            };

    CFTypeRef dataTypeRef = NULL;

    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);

    if (status == errSecSuccess) {
        NSDictionary *dict = (__bridge NSDictionary *)dataTypeRef;
        NSString *acccount = dict[(id)kSecAttrAccount];
        NSData *data = dict[(id)kSecValueData];
        NSString *pwd = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSString *service = dict[(id)kSecAttrService];
        NSLog(@"==result:%@", dict);
    }

这样,我们就得到了这个item的所有属性。

Sharing Items

通一个开发者账号下,各个应用之间可以共享item。keychain通过keychain-access-groups 来进行访问权限的控制。在Xcode的Capabilities选项中打开Keychain Sharing即可。

每个group命名开头必须是开发者账号的teamId。不同开发者账号的teamId是唯一的,所以苹果限制了只有同一个开发者账号下的应用才可以进行共享。如果有多个sharedGroup,在添加的时候如果不指定,默认是第一个group。

添加:

NSDictionary *query = @{(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlocked,  
                            (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
                            (__bridge id)kSecValueData : [@"1234562" dataUsingEncoding:NSUTF8StringEncoding],
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
                            (__bridge id)kSecAttrService : @"noraml1",
                            (__bridge id)kSecAttrSynchronizable : @YES,
                            };

    CFErrorRef error = NULL;

    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, nil);

取:

NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,  
                            (__bridge id)kSecReturnRef : @YES,
                            (__bridge id)kSecReturnData : @YES,
                            (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll,
                            (__bridge id)kSecAttrAccount : @"account name",
                            (__bridge id)kSecAttrAccessGroup : @"XEGH3759AB.com.developer.test",
                            (__bridge id)kSecAttrService : @"noraml1",
                            };

    CFTypeRef dataTypeRef = NULL;

    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &dataTypeRef);

只需要添加一个kSecAttrAccessGroup属性即可。

comments powered by Disqus