美文网首页iOS基础知识
iOS存储技术-Keychain

iOS存储技术-Keychain

作者: 今天写明天改 | 来源:发表于2021-07-29 12:31 被阅读0次

简介

钥匙串这个技术大家每天都在用,它相当于一个容器,里面有已加密的和未加密的用户信息,它是怎么实现安全储存Mac、App、服务器和网站的帐户,开发过程中又该怎么使用这个技术呢。通过一个例子来介绍一下:

需求场景和基础环境

用户要登录你的APP,这个时候用户在文本框输入了他的用户信息和密码,那么你该如何存储这个信息?自然我们会有一个类似用户的结构来存储用户信息

struct Credential {
      var account = ""
      var password = ""
}

这个结构里有一个String类型的用户名称和一个String类型的密码变量, 张三输入了它自己的用户名“zhangsan”和密码“******”来登陆,那么我们就会有一个生成用户的过程

let creden = Credential(account: "zhangsan", password: "******")

那么接下来你希望存储到Keychain中,Keychain有哪些方法呢?第一步自然是添加

添加Keychain

SecItemAdd(_ attributes: CFDictionary, 
              _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus

函数有三个参数,但是每个似乎都不认识,所以我们先简单看一下这三个参数要做什么

添加函数的字典参数

第一个参数是个字典,那自然是由许多key-value构成,首先它要包含一个这个数据的类型,数据类型使用kSecClass来做key,kSecClass的定义:

let kSecClass: CFString

可以看到kSecClass是一个CFString类型的全局变量,它其中可用的值由Item Class Keys and Values列出。根据数据类型的不同有不同的值,例如密码、认证,对于密码它的值定义为:

let kSecClassGenericPassword: CFString

也是一个CFString类型的变量, class对应的值会决定数据是否被加密,当选择这个password的时候数据就会被加密。那么我们字典中的第一个key-value对就有了:

kSecClass as String: kSecClassGenericPassword

那么这个字典还需要包含什么呢? 账号:也就是这个数据是谁的数据,这个属性由kSecAttrAccount这个Key来定义,同样它也是一个CFString类型的Key,它的值是你自定义的一个CFString类型的值。当然这个属性并不是必须的,于是字典中的第二个key-value对也有了

kSecAttrAccount as String: creden.account

⚠️注意

只有class 是kSecClassGenericPassword 和 kSecClassInternetPassword 的时候才有这个属性.

字典中除了用户名还需要用户的数据,数据使用kSecValueData这个Key来定义,同样是CFString类型的Key,但是用户的数据可能多种多样,所以它的值类型是CFData。那么就需要把用户的信息加工一下

let data = creden.password.data(using: String.Encoding.utf8)!

这样就得到了字典中第三个key-value对:

kSecValueData as String: data

既然是钥匙串,那就不能随时随地访问,需要访问控制权限,所谓访问控制就是你希望当iPhone解锁的时候,或者是验证了用户的指纹之后才能继续进行的过程。权限由kSecAttrAccessControl这个Key来表示,它所对应的值是一个SecAccessControl的实例,而SecAccessControl又是什么?

SecAccessControl

它就是一个包含Keychain对象怎么被使用的信息的一个不透明类型,来看看它的实例化

SecAccessControl的实例化

它通过SecAccessControlCreateWithFlags(CFAllocatorRef allocator, CFTypeRef protection, SecAccessControlCreateFlags flags, CFErrorRef _Nullable *error)函数来创建
函数有四个参数:
1.第一个参数是用来初始化SecAccessControlRef对象的. 我们可以传 NULL 或者kCFAllocatorDefault
2.第二个参数是控制设备什么情况下可以访问这个Keychain信息, 它的值可以是添加Keychain函数的第一个参数字典中的一个其它key(kSecAttrAccessible)对应的值,例如可以控制当设备解锁的时候使用的值:kSecAttrAccessibleWhenUnlocked: CFString。其它可使用的还有kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly(只有这台设备且设置了密码)、kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly(只有这个设备第一次解锁后)。
3.第三个参数是一组额外的访问控制:用来控制用户级别的访问权限,如果设备没有密码总是处于unlocked的状态,你可能希望进一步限制KeyChain访问。例如在获取银行账户的认证时候,需要在获取认证信息之前验证是不是授权用户在操作,这使得KeyChain可以根据用户的输入来管理对Keychain的访问,可以选择devicePasscode来限制需要用户需要输入密码或者是选择userPresence来让系统根据当前状态选择一种验证方式或者是多种方式的组合
第四个是失败原因的一个指针,这里暂时传一个nil值
所以我们可以通过SecAccessControlCreateWithFlags来获得一个访问控制的参数

let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags.userPresence, nil) as Any

⚠️注意

因为创建SecAccessControl的时候包含了kSecAttrAccess对应的一些权限控制,所以,在添加Keychain的时候第一个字典中就不应该同时包含kSecAttrAccessControl和kSecAttrAccess这两个Key。一来这两者是互斥的,二来也没有必要重复添加。

现在我们可以回到之前创建函数的第一个参数字典的分析了,我们得到了
SecAccessControl的实例,所以字典的第四个key-value对也有了:

kSecAttrAccessControl as String: accessControl

现在字典中有了数据的类型、哪个用户的数据、用户要存储的数据、什么条件可以访问这个数据。看起来不缺少什么了。我们的一个字典参数就处理好了:

let queryDic = [kSecClass as String: kSecClassGenericPassword,
                        kSecAttrAccessControl as String: accessControl,
                        kSecValueData as String:  creden.password,
                        kSecAttrAccount as String: data
        ] as [String : Any]

添加函数的返回

函数会通过第二个参数来返回新添加的Keychain,具体的类型是根据第一个参数中指定的返回类型决定的(例如可以通过kSecReturnData这个可以指定返回类型为CFData) 当然,通常我们可以忽略这个返回的数据,所以可以传一个nil值
函数还有一个返回值,从声明上看是一个OSStatus类型的值,相应的定义在Security Framework Result Codes中,常见的值有:

  • errSecSuccess 成功
  • errSecParam 参数错误
  • errSecDuplicateItem keychain已经存在

我们通常需要将返回值和已知的返回值相比较来判断是否操作成功了,也就是我们通常可以使用如下的语句来处理添加操作

let status: OSStatus = SecItemAdd(queryDic as CFDictionary, nil)
guard status == errSecSuccess else {
         throw KeychainError.unknow(status: status)
}

至此,添加操作就完成了。

查询Keychain

查询主要使用SecItemCopyMatching(CFDictionaryRef query, CFTypeRef _Nullable *result)函数,函数会返回一个或者多个item,或者是指定的item属性的copy,默认情况下只会返回匹配的第一个结果。

查询函数的字典参数

函数的第一个参数就是和添加Keychain函数的参数一样的结构,通常有Keychain的class也就是由kSecClass为Key的一个key-value对。
属性:属性就是Keychain结果需要符合的条件,例如想查找哪个用户的数据,查询参数还可以带控制返回的key,因为添加方法和查询方法都会返回结果的数据和属性到提供的参数指针里,所以可以指定返回的key来控制指针对应的返回数据的格式,也就是通常的密码查询应该包含kSecReturnData为Key的key-value对。
例如可以使用kSecReturnPersistentRef这个Key来获得一个CFData的引用,然后可以把它存储在磁盘或在进程间传递,可以在这之后调用另一个SecItemCopyMatching函数将持久化引用转为常规引用,函数参数里需要将持久化的引用的数组作为kSecMatchItemList的值传入。如果使用kSecReturnData来控制返回data本身,搜索会返回一个代表实际数据的CFData,这个就是典型的密码Keychain的使用方式。同时,Keychain服务会在返回给你之前对数据进行解密
搜索参数:这个参数可以包含一些结果的数量条件,控制string属性是否大小写敏感等。
所以,希望查询上面的用户信息的时候查询字典参数会如下所示

let queryDic = [kSecClass as String: kSecClassGenericPassword,
                        kSecReturnData as String: true,
                        kSecAttrAccount as String: creden.account] as [String : Any]

查询函数的返回

函数的第二个参数是一个CFTypeRef类型的接收函数返回的指针,我们需要先定义一个这样的指针:

var res: CFTypeRef? = nil

同样我们需要判断函数返回值是否成功:

let status: OSStatus = SecItemCopyMatching(queryDic as CFDictionary, &res)
guard status == errSecSuccess else {
        throw KeychainError.unknow(status: status)
}

因为查询字典参数里携带了kSecReturnData,所以这个指针指向的数据类型是一个CFData类型的参数,我们需要获取对应的值

guard let CFdata = res, let data = CFdata as? Data,  let str = String(data: data, encoding: .utf8) else {
        return 
}

这样 str就是我们之前存储在KeyChain中的用户信息了
至此,Keychain的添加和删除都已经具备了,基本的用户需求就解决了。

高级

除了基础的使用之外,我们还可以

  • 在添加函数的参数字典中添加更多的key-value对来访问信息和访问控制权限进行更多的定义,还可以一次性添加多个信息,
  • 查询的时候,我们可以指定自己的查询源,定义更多的限制条件,
  • 另外查询结果对应的错误抛出和捕获也需要进一步的处理

这些,下次再说吧

相关文章

网友评论

    本文标题:iOS存储技术-Keychain

    本文链接:https://www.haomeiwen.com/subject/vnyqvltx.html