Keychain
是什么?它相当于一个轻量级的数据库,可以把数据存储在Keychain
中,当移动端卸载后又重装,可以做到数据还原。无论是使用系统自带的还是第三方框架,都需要导入系统库:Security.framework
。
关于
Keychain
的文章,网上搜索一搜一大把,可以说全部都是废话,说是可以实现多个App之间实现数据共享,结果没有一篇文章有详细教程如何实现,更是大部分文章的代码完全都是一样的,也不知道是谁抄谁的。不过还是有一些文章是挺有质量的,至少让我知道了一个叫做SAMKeychain的好库,这个库拢共只有两对儿(.h和.m)
文件,如下:
-
SAMKeychain.h
-
SAMKeychainQuery.h
简单的玩儿了玩儿,发现挺不错,于是自己改造了一下,让它变成自己的东西,主要是为了加深自己的理解和方便以后对Keychain
的快速投入使用。
实话实说,最近几天只要是有时间都会看一下关于Keychain
的文章,最后结合了几篇文章再加上自己的理解总结一下。
关于说到使用Keychain
实现多个App之间共享数据的说法,我的测试结果是这样的,如果项目中,没有添加Keychain Sharing
的话,或者说添加了Keychain Sharing
,但是没有添加分组,就像下图这样:
那么保存的数据就只在当前项目的私有空间内,存储的数据所在group
的名称是当前项目加载的开发证书的teamID加上当前项目的bundleID,就像这样:AB0CD12EFG.com.abcdefg.KeyChainTestDemo
。下面是我用SAMKeychain改造之后存储的数据,可以简单看一下:
如果添加了Keychain Sharing
,并且也添加了分组,像下图这样:
那么再次保存的数据将会被保存到新添加的这个分组中,之前添加的数据如果不删除会一直保留,虽然新分组的名称和默认的group
的名称是一样的,但是性质确实不同的,因为只要添加了分组,其他App就可以通过这个组名来获取到当前App存储在这个分组中的所有数据,那么其他App是如果通过group
名称获取数据的呢?重点!只要是同一个开发者账号下的App,需要全部都设置Keychain Sharing
,且group
名称必须相同,就能实现数据共享。在我测试当中,像下图这样:
无论我设置了多少个分组,存储数据和获取数据都只对第0个分组进行操作,我也不知道为什么??所以我的结论是:Keychain Sharing
中,只有第0个分组有效。是不是很尴尬!话说我自己对这个结论都表示怀疑……
分享测试
下面是我的测试demo和针对SAMKeychain改造后的代码,有兴趣的可以看一下
- 1、页面
- 2、测试代码
#import "ViewController.h"
#import "LYKeychain.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *tfAccount;
@property (weak, nonatomic) IBOutlet UITextField *tfService;
@property (weak, nonatomic) IBOutlet UITextField *tfValue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = UIColor.whiteColor;
}
//写入数据
- (IBAction)writeToKeychainAction:(id)sender {
BOOL isBol = [LYKeychain setValue:self.tfValue.text service:self.tfService.text account:self.tfAccount.text];
NSLog(@"isBol = %@",isBol?@"yes":@"no");
}
//读取全部数据
- (IBAction)readAllCacheAction:(id)sender {
NSArray *array = [LYKeychain allAccounts];
NSLog(@"array = %@",array);
}
//删除全部数据
- (IBAction)deleteAllCacheAction:(id)sender {
BOOL isBol = [LYKeychain deleteAllAccounts];
NSLog(@"isBol = %@",isBol?@"yes":@"no");
}
//读取指定数据
- (IBAction)readCacheAction:(id)sender {
NSString *cache = [LYKeychain valueWithService:self.tfService.text account:self.tfAccount.text];
NSLog(@"cache = %@",cache);
}
//删除指定数据
- (IBAction)deleteCacheAction:(id)sender {
BOOL isBol = [LYKeychain deleteValueWithService:self.tfService.text account:self.tfAccount.text];
NSLog(@"isBol = %@",isBol?@"yes":@"no");
}
//取消键盘
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
@end
- 3、改造后的工具
- LYKeychain.h
#import
NS_ASSUME_NONNULL_BEGIN
/**
Error code specific to LYKeychain that can be returned in NSError objects.
For codes returned by the operating system, refer to SecBase.h for your
platform.
*/
typedef NS_ENUM(OSStatus, LYKeychainErrorCode) {
/** Some of the arguments were invalid. */
LYKeychainErrorBadArguments = -1001,
};
/** LYKeychain error domain */
extern NSString *const lyKeychainErrorDomain;
/** Account name. */
extern NSString *const lyKeychainAccountKey;
/**
Time the item was created.
The value will be a string.
*/
extern NSString *const lyKeychainCreatedAtKey;
/** Item class. */
extern NSString *const lyKeychainClassKey;
/** Item description. */
extern NSString *const lyKeychainDescriptionKey;
/** Item label. */
extern NSString *const lyKeychainLabelKey;
/**
Time the item was last modified.
The value will be a string.
*/
extern NSString *const lyKeychainLastModifiedKey;
/** Where the item was created. */
extern NSString *const lyKeychainWhereKey;
@interface LYKeychain : NSObject
/** 字符串类型存储 */
+ (BOOL)setValue:(NSString *)value service:(NSString *)service account:(NSString *)account;
/** Data类型存储 */
+ (BOOL)setValueData:(NSData *)value service:(NSString *)service account:(NSString *)account;
/** 删除缓存 */
+ (BOOL)deleteAllAccounts;
+ (BOOL)deleteValueWithService:(NSString *)service account:(NSString *)account;
/** 读取全部 */
+ (nullable NSArray *> *)allAccounts;
+ (nullable NSString *)valueWithService:(NSString *)service account:(NSString *)account;
+ (nullable NSData *)valueDataWithService:(NSString *)service account:(NSString *)account;
#pragma mark - Configuration
#if __IPHONE_4_0 && TARGET_OS_IPHONE
/**
Returns the accessibility type for all future passwords saved to the Keychain.
@return Returns the accessibility type.
The return value will be `NULL` or one of the "Keychain Item Accessibility
Constants" used for determining when a keychain item should be readable.
@see setAccessibilityType
*/
+ (CFTypeRef)accessibilityType;
/**
Sets the accessibility type for all future passwords saved to the Keychain.
@param accessibilityType One of the "Keychain Item Accessibility Constants"
used for determining when a keychain item should be readable.
If the value is `NULL` (the default), the Keychain default will be used which
is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
for background applications or `kSecAttrAccessibleWhenUnlocked` for all
other applications.
@see accessibilityType
*/
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
#endif
@end
NS_ASSUME_NONNULL_END
- LYKeychain.m
#import "LYKeychain.h"
#import "LYKeychainQuery.h"
NSString *const lyKeychainErrorDomain = @"com.samsoffes.samkeychain";
NSString *const lyKeychainAccountKey = @"acct";
NSString *const lyKeychainCreatedAtKey = @"cdat";
NSString *const lyKeychainClassKey = @"labl";
NSString *const lyKeychainDescriptionKey = @"desc";
NSString *const lyKeychainLabelKey = @"labl";
NSString *const lyKeychainLastModifiedKey = @"mdat";
NSString *const lyKeychainWhereKey = @"svce";
#if __IPHONE_4_0 && TARGET_OS_IPHONE
static CFTypeRef LYKeychainAccessibilityType = NULL;
#endif
@implementation LYKeychain
+ (BOOL)setValue:(NSString *)value service:(NSString *)service account:(NSString *)account {
return [self setValue:value service:service account:account error:nil];
}
+ (BOOL)setValue:(NSString *)value service:(NSString *)service account:(NSString *)account error:(NSError *__autoreleasing *)error {
if (!value.length || !service.length || !account.length) {
return NO;
}
LYKeychainQuery *query = [[LYKeychainQuery alloc] init];
query.service = service;
query.account = account;
query.password = value;
return [query save:error];
}
+ (BOOL)setValueData:(NSData *)value service:(NSString *)service account:(NSString *)account {
return [self setValueData:value service:service account:account error:nil];
}
+ (BOOL)setValueData:(NSData *)value service:(NSString *)service account:(NSString *)account error:(NSError **)error {
LYKeychainQuery *query = [[LYKeychainQuery alloc] init];
query.service = service;
query.account = account;
query.passwordData = value;
return [query save:error];
}
+ (BOOL)deleteAllAccounts {
return [self deleteValueWithService:nil account:nil error:nil deleteAll:YES];
}
+ (BOOL)deleteValueWithService:(NSString *)service account:(NSString *)account {
return [self deleteValueWithService:service account:account error:nil deleteAll:NO];
}
+ (BOOL)deleteValueWithService:(nullable NSString *)service account:(nullable NSString *)account error:(NSError *__autoreleasing *)error deleteAll:(BOOL)isAll{
LYKeychainQuery *query = [[LYKeychainQuery alloc] init];
//指定删除时执行此操作
if (!isAll) {
query.service = service;
query.account = account;
}
return [query deleteItem:error deleteAll:isAll];
}
+ (nullable NSArray *)allAccounts {
return [self service:nil account:nil error:nil class:NSArray.class];
}
+ (nullable NSString *)valueWithService:(NSString *)service account:(NSString *)account {
return [self service:service account:account error:nil class:NSString.class];
}
+ (nullable NSData *)valueDataWithService:(NSString *)service account:(NSString *)account {
return [self service:service account:account error:nil class:NSData.class];
}
+ (nullable id)service:(nullable NSString *)service account:(nullable NSString *)account error:(NSError *__autoreleasing *)error class:(Class)class {
LYKeychainQuery *query = [[LYKeychainQuery alloc] init];
if ([class isSubclassOfClass:NSArray.class]) {
return [query fetchAll:error];
}else if ([class isSubclassOfClass:NSString.class]){
query.service = service;
query.account = account;
[query fetch:error];
return query.password;
}else if ([class isSubclassOfClass:NSData.class]){
query.service = service;
query.account = account;
[query fetch:error];
return query.passwordData;
}
return nil;
}
#if __IPHONE_4_0 && TARGET_OS_IPHONE
+ (CFTypeRef)accessibilityType {
return LYKeychainAccessibilityType;
}
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
CFRetain(accessibilityType);
if (LYKeychainAccessibilityType) {
CFRelease(LYKeychainAccessibilityType);
}
LYKeychainAccessibilityType = accessibilityType;
}
#endif
@end
- LYKeychainQuery.h
#if __has_feature(modules)
@import Foundation;
@import Security;
#else
#import
#import
#endif
NS_ASSUME_NONNULL_BEGIN
#if __IPHONE_7_0 || __MAC_10_9
// Keychain synchronization available at compile time
#define SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
#endif
#if __IPHONE_3_0 || __MAC_10_9
// Keychain access group available at compile time
#define SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
typedef NS_ENUM(NSUInteger, LYKeychainQuerySynchronizationMode) {
LYKeychainQuerySynchronizationModeAny,
LYKeychainQuerySynchronizationModeNo,
LYKeychainQuerySynchronizationModeYes
};
#endif
@interface LYKeychainQuery : NSObject
/** kSecAttrAccount */
@property (nonatomic, copy, nullable) NSString *account;
/** kSecAttrService */
@property (nonatomic, copy, nullable) NSString *service;
/** kSecAttrLabel */
@property (nonatomic, copy, nullable) NSString *label;
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
/** kSecAttrAccessGroup (only used on iOS) */
@property (nonatomic, copy, nullable) NSString *accessGroup;
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/** kSecAttrSynchronizable */
@property (nonatomic) LYKeychainQuerySynchronizationMode synchronizationMode;
#endif
/** Root storage for password information */
@property (nonatomic, copy, nullable) NSData *passwordData;
/**
This property automatically transitions between an object and the value of
`passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
*/
@property (nonatomic, copy, nullable) id passwordObject;
/**
Convenience accessor for setting and getting a password string. Passes through
to `passwordData` using UTF-8 string encoding.
*/
@property (nonatomic, copy, nullable) NSString *password;
/**
Save the receiver's attributes as a keychain item. Existing items with the
given account, service, and access group will first be deleted.
@param error Populated should an error occur.
@return `YES` if saving was successful, `NO` otherwise.
*/
- (BOOL)save:(NSError **)error;
/**
Delete keychain items that match the given account, service, and access group.
@param error Populated should an error occur.
@return `YES` if saving was successful, `NO` otherwise.
*/
- (BOOL)deleteItem:(NSError **)error deleteAll:(BOOL)isAll;
/**
Fetch all keychain items that match the given account, service, and access
group. The values of `password` and `passwordData` are ignored when fetching.
@param error Populated should an error occur.
@return An array of dictionaries that represent all matching keychain items or
`nil` should an error occur.
The order of the items is not determined.
*/
- (nullable NSArray *> *)fetchAll:(NSError **)error;
/**
Fetch the keychain item that matches the given account, service, and access
group. The `password` and `passwordData` properties will be populated unless
an error occurs. The values of `password` and `passwordData` are ignored when
fetching.
@param error Populated should an error occur.
@return `YES` if fetching was successful, `NO` otherwise.
*/
- (BOOL)fetch:(NSError **)error;
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/**
Returns a boolean indicating if keychain synchronization is available on the device at runtime. The #define
SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time. If you are checking for the presence of synchronization,
you should use this method.
@return A value indicating if keychain synchronization is available
*/
+ (BOOL)isSynchronizationAvailable;
#endif
@end
NS_ASSUME_NONNULL_END
- LYKeychainQuery.m
#import "LYKeychainQuery.h"
#import "LYKeychain.h"
@implementation LYKeychainQuery
@synthesize account = _account;
@synthesize service = _service;
@synthesize label = _label;
@synthesize passwordData = _passwordData;
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
@synthesize accessGroup = _accessGroup;
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
@synthesize synchronizationMode = _synchronizationMode;
#endif
#pragma mark - Public
- (BOOL)save:(NSError *__autoreleasing *)error {
OSStatus status = LYKeychainErrorBadArguments;
if (!self.service || !self.account || !self.passwordData) {
if (error) {
*error = [[self class] errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = nil;
NSMutableDictionary * searchQuery = [self query];
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
if (status == errSecSuccess) {//item already exists, update it!
query = [[NSMutableDictionary alloc]init];
[query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [LYKeychain accessibilityType];
if (accessibilityType) {
[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
}
#endif
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}else if(status == errSecItemNotFound){//item not found, create it!
query = [self query];
if (self.label) {
[query setObject:self.label forKey:(__bridge id)kSecAttrLabel];
}
[query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [LYKeychain accessibilityType];
if (accessibilityType) {
[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
}
#endif
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [[self class] errorWithCode:status];
}
return (status == errSecSuccess);
}
- (BOOL)deleteItem:(NSError *__autoreleasing _Nullable *)error deleteAll:(BOOL)isAll {
OSStatus status = LYKeychainErrorBadArguments;
//指定删除时执行此操作
if (!isAll) {
if (!self.service || !self.account) {
if (error) {
*error = [[self class] errorWithCode:status];
}
return NO;
}
}
NSMutableDictionary *query = [self query];
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
// On Mac OS, SecItemDelete will not delete a key created in a different
// app, nor in a different version of the same app.
//
// To replicate the issue, save a password, change to the code and
// rebuild the app, and then attempt to delete that password.
//
// This was true in OS X 10.6 and probably later versions as well.
//
// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
CFTypeRef result = NULL;
[query setObject:@YES forKey:(__bridge id)kSecReturnRef];
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
if (status != errSecSuccess && error != NULL) {
*error = [[self class] errorWithCode:status];
}
return (status == errSecSuccess);
}
- (nullable NSArray *)fetchAll:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self query];
[query setObject:@YES forKey:(__bridge id)kSecReturnAttributes];
[query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [LYKeychain accessibilityType];
if (accessibilityType) {
[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
}
#endif
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess && error != NULL) {
*error = [[self class] errorWithCode:status];
return nil;
}
return (__bridge_transfer NSArray *)result;
}
- (BOOL)fetch:(NSError *__autoreleasing *)error {
OSStatus status = LYKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [[self class] errorWithCode:status];
}
return NO;
}
CFTypeRef result = NULL;
NSMutableDictionary *query = [self query];
[query setObject:@YES forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
if (error) {
*error = [[self class] errorWithCode:status];
}
return NO;
}
self.passwordData = (__bridge_transfer NSData *)result;
return YES;
}
#pragma mark - Accessors
- (void)setPasswordObject:(id)object {
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object requiringSecureCoding:YES error:nil];
}
- (id)passwordObject {
if ([self.passwordData length]) {
return [NSKeyedUnarchiver unarchivedObjectOfClass:[NSData class] fromData:self.passwordData error:nil];
}
return nil;
}
- (void)setPassword:(NSString *)password {
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)password {
if ([self.passwordData length]) {
return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
}
return nil;
}
#pragma mark - Synchronization Status
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+ (BOOL)isSynchronizationAvailable {
#if TARGET_OS_IPHONE
// Apple suggested way to check for 7.0 at runtime
// https://developer.apple.com/library/ios/documentation/userexperience/conceptual/transitionguide/SupportingEarlieriOS.html
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1;
#else
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
#endif
}
#endif
#pragma mark - Private
- (NSMutableDictionary *)query {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
[dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
if (self.service) {
[dictionary setObject:self.service forKey:(__bridge id)kSecAttrService];
}
if (self.account) {
[dictionary setObject:self.account forKey:(__bridge id)kSecAttrAccount];
}
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
if (self.accessGroup) {
[dictionary setObject:self.accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
}
#endif
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([[self class] isSynchronizationAvailable]) {
id value;
switch (self.synchronizationMode) {
case LYKeychainQuerySynchronizationModeNo: {
value = @NO;
break;
}
case LYKeychainQuerySynchronizationModeYes: {
value = @YES;
break;
}
case LYKeychainQuerySynchronizationModeAny: {
value = (__bridge id)(kSecAttrSynchronizableAny);
break;
}
}
[dictionary setObject:value forKey:(__bridge id)(kSecAttrSynchronizable)];
}
#endif
return dictionary;
}
+ (NSError *)errorWithCode:(OSStatus) code {
static dispatch_once_t onceToken;
static NSBundle *resourcesBundle = nil;
dispatch_once(&onceToken, ^{
NSURL *url = [[NSBundle bundleForClass:[LYKeychainQuery class]] URLForResource:@"LYKeychain" withExtension:@"bundle"];
resourcesBundle = [NSBundle bundleWithURL:url];
});
NSString *message = nil;
switch (code) {
case errSecSuccess: return nil;
case LYKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"LYKeychainErrorBadArguments", @"LYKeychain", resourcesBundle, nil); break;
#if TARGET_OS_IPHONE
case errSecUnimplemented: {
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecParam: {
message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecAllocate: {
message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecNotAvailable: {
message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecDuplicateItem: {
message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecItemNotFound: {
message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecInteractionNotAllowed: {
message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecDecode: {
message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"LYKeychain", resourcesBundle, nil);
break;
}
case errSecAuthFailed: {
message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"LYKeychain", resourcesBundle, nil);
break;
}
default: {
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"LYKeychain", resourcesBundle, nil);
}
#else
default:
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
#endif
}
NSDictionary *userInfo = nil;
if (message) {
userInfo = @{ NSLocalizedDescriptionKey : message };
}
return [NSError errorWithDomain:lyKeychainErrorDomain code:code userInfo:userInfo];
}
@end
所有代码,复制黏贴可运行