Keychain Services -- kSecClassGenericPassword

今天做一个需求,要求获取到设备的IDFA,但是我们都知道,这个值是会变的,会受到用户的影响,所以就想看能不能做一个持久化(对于用户不允许的情况,可以自己生成一个,实现有具体的开源代码),随着软件的卸载和再安装,这个值始终是不变的。
就想起来了keychain--这个系统级的存储。这也就是为什么有些软件在卸载后重新安装后用户名和密码还在的方法之一(因为还有其他方法)。

除了做数据存储,其实它还可以做APP间的数据共享。

因为这部分概念的东西并不多,所以下面将直接上代码,遇到问题顺带着再说;另:mac和iOS不同,这里只说iOS的。

所有的详细资料在这里

一、Keychain

1、概念

Keychain是一个储存在文件下的简单数据库。通常情况下,app里有一个简单的keychain,可以被所有的app使用。
Keychain有任意数量的钥匙链(item),该钥匙链里包含一组属性。该属性和钥匙链的类型相关。创建日期和label对所有的钥匙链是通用的。其他的都是根据钥匙链的类型不同而不同,比如,generic password类型包含service和account属性。
钥匙链可以使用kSecAttrSynchronizable同步属性,被标记为该属性的值都可以被放置在iCloud的钥匙链中,它会被自动同步到相同账号的设备上。
有些钥匙链需要保护起来,比如密码和私人key,都会被加密;对于那些不需要被保护的钥匙链,比如证书,就不会被加密。
在iOS设备上(手机),当屏幕被解锁时,钥匙链的访问权限就会被打开。

2、访问

首先说明其能保存的类型,有5种:
  • kSecClassGenericPassword:存储一般密码,比较常用这个
  • kSecClassInternetPassword:存储网络密码
  • kSecClassCertificate:存储证书
  • kSecClassKey:存储私有密钥
  • kSecClassIdentity:存储一个包含证书和私有密钥的item
(1)添加
  • 方法:OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
      attributes有四部分组成:
      a、item的类型,必选,key=kSecClass,value就是上面说的5种;
      b、item的存储数据;
      c、属性,可以用来做一些标记用于查找或者程序之间的数据分享,但是不同的kSecClass有不同的属性;
      d、返回数据类型,它的设置影响参数result。
      result:对添加的item的引用,如果没有什么需要,可以设为nil
  • 使用:
- (void)saveBasicInfo {
    CFMutableDictionaryRef mutableDicRef = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(mutableDicRef, kSecClass, kSecClassGenericPassword);  //类型
    CFDateRef dateRef = CFDateCreate(kCFAllocatorDefault, CFAbsoluteTimeGetCurrent());
    CFDictionarySetValue(mutableDicRef, kSecAttrCreationDate, dateRef);  //创建时间
    CFStringRef strRef = CFSTR("save generic password 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrDescription, strRef);  //描述
    CFStringRef commentStrRef = CFSTR("generic password comment 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrComment, commentStrRef); //备注、注释
    CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
    CFDictionarySetValue(mutableDicRef, kSecAttrCreator, creatorRef);  //创建者,只能是四个字符长度
    CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
    CFDictionarySetValue(mutableDicRef, kSecAttrType, typeRef);   // 类型
    CFStringRef labelRef = CFSTR("label 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrLabel, labelRef); //标签,用户可以看到
    CFDictionarySetValue(mutableDicRef, kSecAttrIsInvisible, kCFBooleanTrue);  //是否不可见、是否隐藏(kCFBooleanTrue、kCFBooleanFalse)
    CFDictionarySetValue(mutableDicRef, kSecAttrIsNegative, kCFBooleanFalse); //标记是否有密码(kCFBooleanTrue、kCFBooleanFalse)
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(mutableDicRef, kSecAttrAccount, accountRef);  //账户,相同的账户不允许储存两次,否则会报错
    CFStringRef serviceRef = CFSTR("service");
    CFDictionarySetValue(mutableDicRef, kSecAttrService, serviceRef);  //所具有的服务
    CFMutableDataRef genericRef = CFDataCreateMutable(kCFAllocatorDefault, 0);
    char * generic_char = "personal generic 2";
    CFDataAppendBytes(genericRef, (const UInt8 *)generic_char, sizeof("personal generic 2"));
    CFDictionarySetValue(mutableDicRef, kSecAttrGeneric, genericRef);  //用户自定义
    CFDictionarySetValue(mutableDicRef, kSecValueData, genericRef);  //保存值
    OSStatus status = SecItemAdd(mutableDicRef, nil);  //相同的东西只能添加一次,不能重复添加,重复添加会报错
    if (status == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"%@",@(status));
    }
... //神略的是前面创建的变量的释放
}
  • 总结:
      1、要保证每次添加都是不同账户
      2、注意变量内存释放
      3、要注意数据类型要对
(2)查找(最好的办法是根据用户名查找)
  • 方法:OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
      query的组成:
      a、item的类型,必选,key=kSecClass,value就是上面说的5种;
      b、要查找的属性以及值,这个可以用来筛选掉多余的选项,这个值越详细,最后获取的结果越精确
      c、做进一步的限制,比如大小写是否敏感等等
      d、返回匹配项的个数,但是注意kSecReturnData
    和kSecMatchLimit不能共存,更多的kSecMatchLimit可以搭配kSecReturnAttributes使用
  • 使用:
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找类型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    //匹配的属性 越详细,越精准
//    CFStringRef strRef = CFSTR("save generic password");
//    CFDictionarySetValue(queryDic, kSecAttrDescription, strRef);  //描述
//    CFStringRef commentStrRef = CFSTR("generic password comment");
//    CFDictionarySetValue(queryDic, kSecAttrComment, commentStrRef); //备注、注释
//    CFNumberRef creatorRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "zhou");
//    CFDictionarySetValue(queryDic, kSecAttrCreator, creatorRef);  //创建者,只能是四个字符长度
//    CFNumberRef typeRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, "type");
//    CFDictionarySetValue(queryDic, kSecAttrType, typeRef);   // 类型
//    CFStringRef labelRef = CFSTR("label");
//    CFDictionarySetValue(queryDic, kSecAttrLabel, labelRef); //标签,用户可以看到
    //查找的参数
//    CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitAll);  //可以控制当key=kSecReturnAttributes时返回值的个数
    //返回类型
//    CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
            NSLog(@"Dictionary");
        }
    } else {
        NSLog(@"%@",@(status));
    }
...//神略的是前面创建的变量的释放
}
  • 总结:
    这里主要注意一下返回类型就好了!!!
(3)删除
  • 方法:OSStatus SecItemDelete(CFDictionaryRef query)
    query可以直接自己创建也可以是查找后获得的,具体使用看下面的使用。
  • 使用:
方法一:
//直接删除
- (void)deleteItemDirect {
   //删除CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
   CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
   CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);  //这个不要丢!!!
   CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
   CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
   if (accountRef) {
       CFRelease(accountRef);
   }
   OSStatus delStatus = SecItemDelete(queryDic);
   if (delStatus == errSecSuccess) {
       NSLog(@"delete success");
   } else {
       NSLog(@"%@",@(delStatus));
   }
}
方法二:
//先查找,再删除
- (void)deleteItemWithQuery {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue);  //这一句话必须要有,否则删除不了
//    CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue);  //不能用这句,这句是做什么用的呢?
    CFTypeRef result = NULL;
    OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
    if (queryDic) {
        CFRelease(queryDic);
    }
    if (queryStatus != errSecSuccess) {
        NSLog(@"query failed");
        return;
    }
    if (CFGetTypeID(result) == CFDictionaryGetTypeID()) {
        OSStatus delStatus = SecItemDelete(result);
        if (delStatus == errSecSuccess) {
            NSLog(@"delete success");
        } else {
            NSLog(@"delete failed:%@",@(delStatus));
        }
    } else if (CFGetTypeID(result) == CFArrayGetTypeID()) {
        CFArrayRef arrRef = result;
        for (CFIndex i = 0; i < CFArrayGetCount(arrRef); i++) {
            OSStatus delStatus = SecItemDelete(CFArrayGetValueAtIndex(arrRef, i));
            if (delStatus == errSecSuccess) {
                NSLog(@"delete success");
            } else {
                NSLog(@"delete failed:%@",@(delStatus));
            }
        }
    }
}
  • 总结:
    1、不要忘记设置item的类型
    2、如果是先查找然后再删除,则需要加上kSecReturnRef或者kSecReturnPersistentRef(这个还没搞懂)
(4)、更新
  • 方法:OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate)
    attributesToUpdate:包含需要去更新的
    在使用上,同样有可以直接定义,然后去更新(其实其在内部先查找了);也可以先查找,再把查找到的作为参数去更新。
  • 使用:
方法一:
//更新
- (void)updateItem {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFStringRef strRef = CFSTR("save generic password update");
    CFDictionarySetValue(updateDic, kSecAttrDescription, strRef);  //描述
    OSStatus updateStatus = SecItemUpdate(queryDic, updateDic);
    if (updateStatus == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"update Failed:%@",@(updateStatus));
    }
    CFRelease(queryDic);
    CFRelease(updateDic);
}
方法二:
- (void)updateItemAfterSearch {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);
    if (accountRef) {
        CFRelease(accountRef);
    }
    
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFDictionarySetValue(queryDic, kSecReturnRef, kCFBooleanTrue);  //这一句话必须要有,否则删除不了
    //    CFDictionarySetValue(queryDic, kSecReturnPersistentRef, kCFBooleanTrue);  //不能用这句,这句是做什么用的呢?
    CFTypeRef result = NULL;
    OSStatus queryStatus = SecItemCopyMatching(queryDic, &result);
    if (queryDic) {
        CFRelease(queryDic);
    }
    if (queryStatus != errSecSuccess) {
        NSLog(@"query failed");
        return;
    }
    if (CFGetTypeID(result) != CFDictionaryGetTypeID()) {
        return;
    }
    CFMutableDictionaryRef updateDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFStringRef strRef = CFSTR("save generic password update with query");
    CFDictionarySetValue(updateDic, kSecAttrDescription, strRef);  //描述
    OSStatus updateStatus = SecItemUpdate(result, updateDic);
    if (updateStatus == errSecSuccess) {
        NSLog(@"success");
    } else {
        NSLog(@"update Failed:%@",@(updateStatus));
    }
    CFRelease(updateDic);
}
(5)获取值
  • 返回属性kSecReturnAttributes
    属性的返回都是以集合形式返回,即要么是数组+字典,或者就只有字典(默认),所以只要确定了返回类型,然后按照key读取就可以了。如下:
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找类型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);  //账户
    //查找的参数
    CFDictionarySetValue(queryDic, kSecMatchLimit, kSecMatchLimitOne);  //可以控制当key=kSecReturnAttributes时返回值的个数
    //返回类型
    CFDictionarySetValue(queryDic, kSecReturnAttributes, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
            NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
        }
    } else {
        NSLog(@"%@",@(status));
    }
    if (queryDic) {
        CFRelease(queryDic);
    }
    return status;
}
  • 返回值为kSecReturnData
- (OSStatus)keychainMatching {
    CFMutableDictionaryRef queryDic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    //查找类型
    CFDictionarySetValue(queryDic, kSecClass, kSecClassGenericPassword);
    CFStringRef accountRef = CFSTR("zhoupengzu_basic 2");
    CFDictionarySetValue(queryDic, kSecAttrAccount, accountRef);  //账户
    //查找的参数
    //返回类型
    CFDictionarySetValue(queryDic, kSecReturnData, kCFBooleanTrue);
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching(queryDic, &result);
    if (status == errSecSuccess) {
        NSLog(@"success:%@",result);
        if (CFGetTypeID(result) == CFDictionaryGetTypeID()) { //类型判断
            NSLog(@"label:%@",CFDictionaryGetValue(result, kSecAttrDescription));
        } else if (CFGetTypeID(result) == CFDataGetTypeID()) {
            const UInt8 * str = CFDataGetBytePtr(result);
            NSLog(@"%s",str);
        }
    } else {
        NSLog(@"%@",@(status));
    }
    if (queryDic) {
        CFRelease(queryDic);
    }
    return status;
}
  • 其他(kSecReturnRef和kSecReturnPersistentRef)
    这两个我暂时还没搞懂,搞懂了补上!!!

以上代码在这里

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容