iOS安全基础之钥匙串与哈希

2018-08-29 11:03:38 浏览数 (1)

前言

本文最初是由Chris Lowe编写的,后来经过Ryan Ackermann(ios系统开发者)的修改,已经可以针对最新的Xcode 9.2,Swift 4,iOS 11和iPhone X了。

软件开发最重要的一个方面同时也被认为是最核心的部分就是应用程序如何实现更好的安全性。用户都希望他们的应用程序能够安全运行,以避免受潜在的威胁。

我会在本文中,为你仔细讲解iOS安全的基础知识。在了解的同时,我还会告诉你如何使用一些基本的加密哈希方法来安全地将你的输入信息存储在iOS钥匙串中,这样一来,你数据的私密性和受保护程度都将大幅度提高。

我查了一下,目前苹果公司共提供了几个API来帮助用户提高其所使用的应用程序安全,并且你将在使用钥匙串时探索这些API。另外,你将使用CryptoSwift – 一个经过良好审查的开源库,实现加密算法。

入门知识

如果你是ios的初学者,你点此下载基本的安全知识介绍。

材料中所列举的那个应用程序样本是允许用户登录并查看其好友照片的,目前你正在使用的大部分应用程序已经涉及了你的个人隐私,所以本文中,你的工作就是确保应用程序的安全。

下载资料在解压后,请确保打开Friendvatars.xcworkspace包含了所有CocoaPod依赖项。如下所示,构建并运行该应用程序后,你将打开一个登录屏幕。

不过此时,当你点击登录按钮时没有任何反应,这是因为用户的凭证还没有办法进行保存。因此,你要做的第一件事就是要先添加用户的凭证。

为什么安全是苹果的重中之重?

在深入了解代码之前,你应该明白为什么你的应用程序需要强有力的安全保证。如果你要存储比较隐私的用户数据,如电子邮件,密码或银行帐户信息,则应用程序的安全性尤其重要。

但对苹果来说,随着系统的更新换代,安全信息可不止以上这些,从你拍摄的照片到当天记录的健康数据,如行走步数,你的iPhone会存储大量更加个性化的数据,因此这些数据是否安全,就显得非常重要。

俗话说“知己知彼,方能百战百胜”,既然有威胁,那iOS生态系统中的攻击者是谁?他们想要什么?攻击者可能是犯罪分子,商业竞争者,甚至是朋友或亲戚,而且每个攻击者想要的内容都不一样。有些攻击者可能想要盗取用户隐私信息已进行牟利,而另一些人可能想看用户的手机中存有什么有价值的商业机密。

所以我要在重复一遍,确保应用程序保存的数据免受潜在威胁的影响是你阅读本文的目的。幸运的是,你不需要像软件开发者那样从头架构一个新的安全框架,苹果已经构建了许多强大的API来简化这项任务。

苹果的钥匙串

iOS开发人员最重要的安全手段之一就是钥匙串,从iOS3.0开始,系统就提供了钥匙串作为存储账号,密码,网络密码,认证令牌的工具。每个应用程序的钥匙串相对来说是独立的,但是在一些情况下也可以实现应用程序之间钥匙串数据的共享,前提是必须同一个TeamID下的应用。简而言之,它是存储元数据和敏感信息的专用数据库,使用钥匙串是存储对你的应用至关重要的小块数据(如秘密和密码)的最佳做法。

为什么要使用钥匙串来作为安全解决方案?难道仅仅是因为在UserDefaults中不存储base-64编码的用户密码吗?当然不是!对于攻击者来说,恢复以这种方式存储的密码简直再简单不过了,如果是这样,那安全性就很难保证了。如果你尝试自己来自定义一套安全解决方案也不是一个好主意。即使你的应用程序不涉及金融信息,存储私人用户信息也不应该掉以轻心。

不过,要直接与钥匙串进行交互,那是相当复杂的,尤其是在Swift中,因为你必须使用主要由C语言编写的安全框架。

幸运的是,你可以通过从下载材料中的样本代码GenericKeychain借用Swift封装器来避免使用这些低级API。就在下载材料中,KeychainPasswordItem已为钥匙串提供了一个易于使用的Swift接口。

使用钥匙串

打开AuthViewController.swift,该视图控制器会负责你最初看到的登录表单。如果向下滚动到Actions部分,你会注意到signInButtonPressed没有做任何事情。所以你需要花点时间,来做一些小的修改,你可以将以下内容添加到Helpers的底部:

代码语言:javascript复制
private func signIn() {
  // 1
  view.endEditing(true)
   
  // 2
  guard let email = emailField.text, email.count > 0 else {
    return
  }
  guard let password = passwordField.text, password.count > 0 else {
    return
  }
   
  // 3
  let name = UIDevice.current.name
  let user = User(name: name, email: email)
}

接下来会发生以下改变:

1.你可以通过关闭键盘操作来避免用户的操作行为被人追踪;

2.你可以接收用户输入的电子邮件和密码,如果Eithe类是零长度,那么你就不要继续往下。在真实的应用程序中,此时用户就会收到错误提示。

3.你可以为用户分配一个名称,就本文而言,你可以从设备名称中分配一个名称。

注意:你可以进入“系统偏好设置▸共享”并在顶部更改计算机名称来更改你的Mac的名称(由sim使用)。此外,你可以进入 “设置▸常规▸关于▸名称”来更改iPhone的名称。

现在在signInButtonPressed中添加以下内容:

代码语言:javascript复制
signIn()

当signInButtonPressed被触发时,会调用你的signIn方法, 找到textFieldShouldReturn并将 case TextFieldTag.password.rawValue中的break替换为以下内容。

代码语言:javascript复制
signIn()

现在signIn()被调用,当用户在键盘上点击返回时,密码字段就会出现焦点并包含文本。不过此时,signIn()尚未完成。你仍然需要存储用户对象以及密码,这些都会在helper类中实现。

打开AuthController.swift,这是一个静态类,它将保存与此应用程序的身份验证相关的逻辑。

首先,在isSignedIn以上的文件顶部添加以下内容:

代码语言:javascript复制
static let serviceName = "FriendvatarsService"

现在signIn()被调用,当用户在键盘上点击返回时,密码字段就会出现焦点并包含文本。不过此时,signIn()尚未完成。你仍然需要存储用户对象以及密码,这些都会在helper类中实现。

打开AuthController.swift,这是一个静态类,它将保存与此应用程序的身份验证相关的逻辑。

首先,在isSignedIn以上的文件顶部添加以下内容:

代码语言:javascript复制
class func signIn(_ user: User, password: String) throws {
  try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(password)
   
  Settings.currentUser = user
}

此方法会将用户的登录信息安全地存储在钥匙串中,然后创建了一个KeychainPasswordItem,其中包含你定义的服务名称和唯一标识符(帐户)。

对于这个应用程序样本,用户的电子邮件会被用作钥匙串的标识符,但对其他样本来说也可以是唯一的用户标识或用户名。最后,Settings.currentUser由存储在UserDefaults中的 user设置的。

不过,此方法并不是最完美的,因为直接存储用户密码并不是最安全的做法。例如,如果攻击者破坏了苹果的钥匙串,他就可以用纯文本形式读取用户的密码。所以更好的解决方案是存储由用户身份构建的哈希。

在AuthController.swift的顶部,由Foundation导入以下添加内容:

代码语言:javascript复制
import CryptoSwift

CryptoSwift是用Swift编写的许多标准加密算法中最受欢迎的集合之一,不过加密过程是个技术活,需要正确地使用才可以。。使用一个流行的安全库意味着你不必从头在设计一遍那些标准化的哈希函数,最好的加密技术是向公众开放的。

注意:苹果的CommonCrypto框架为你提供了许多有用的哈希函数,但在Swift中与它进行交互并不容易。这就是为什么我们选CryptoSwift库的原因。

接下来添加以下的signIn:

代码语言:javascript复制
class func passwordHash(from email: String, password: String) -> String {
  let salt = "x4vV8bGgqqmQwgCoyXFQj (o.nUNQhVP7ND"
  return "(password).(email).(salt)".sha256()
}

实现这种方法的前提是需要一个电子邮件和密码,并返回一个哈希字符串。通过加入盐值(salt)即盐化可以用来制作通用密码的唯一字符串。 sha256()是一种CryptoSwift方法,可以在输入字符串上完成SHA-2哈希。

在前面我讲过,攻击者可以通过泄露了钥匙串发现这个哈希。攻击者可能会创建一个常用密码表及其哈希表来与此哈希进行比较。如果你没有进行盐化处理,那么输入的哈希密码照样会被攻击。盐化会增加攻击的复杂性,此外,你可以将用户的电子邮件和密码与盐化值结合在一起以创建一个不易被破解的哈希。

注意:对于使用服务器后端进行身份验证,应用程序和服务器将共享相同的盐化值,这就允许他们以相同的方式构建哈希并比较两个哈希来验证身份。

返回signIn(_:password:),将调用savePassword的行替换为:

代码语言:javascript复制
let finalHash = passwordHash(from: user.email, password: password)
try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(finalHash)

这样signIn现在就存储了一个强大的哈希,而不是一个原始密码。现在是时候将其添加到视图控制器了, 返回AuthViewController.swift并将以下内容添加到signIn()的底部。

代码语言:javascript复制
do {
  try AuthController.signIn(user, password: password)
} catch {
  print("Error signing in: (error.localizedDescription)")
}

虽然这会存储用户并保存哈希密码,但当身份认证更改时,AppController.swift需要提前得到通知,所以此时用户要登录应用程序就比较慢了。

你可能已经注意到AuthController.swift有一个名为isSignedIn的静态变量。目前,即使用户登录,它总是返回false。

在AuthController.swift中,将isSignedIn更新为以下内容:

代码语言:javascript复制
static var isSignedIn: Bool {
  // 1
  guard let currentUser = Settings.currentUser else {
    return false
  }
   
  do {
    // 2
    let password = try KeychainPasswordItem(service: serviceName, account: currentUser.email).readPassword()
    return password.count > 0
  } catch {
    return false
  }
}

接下来会发生以下改变:

1.你可以马上检查存储在UserDefaults中的当前用户,如果没有存储的用户,就不会有一个标识符来查找来自钥匙串中的密码哈希,这就代表用户没有登录。

2.你可以从钥匙串中读取密码哈希,如果密码存在且不为空,则就表示该用户已登录。

现在,AppController.swift中的handleAuthState将正常工作,但登录应用程序后才能正确更新UI。否则,只能通知应用程序更改状态(如身份验证)。

将以下内容添加到AuthController.swift的底部:

代码语言:javascript复制
extension Notification.Name {
   
  static let loginStatusChanged = Notification.Name("com.razeware.auth.changed")
   
}

在编写自定义通知时使用反向域标识符是一种很好的做法,这通常来自于应用程序的bundle标识符。使用唯一标识符可以在调试时提供帮助,这样任何与你的通知相关的内容都可以从日志中提到的其他框架中被提取出来。

若要使用自定义的通知名称,请将以下内容添加到signIn(_:password:)的底部:

代码语言:javascript复制
NotificationCenter.default.post(name: .loginStatusChanged, object: nil)

这样该通知就会被应用程序的其他部分被看见,在AppController.swift的内部,你可以在show(in:)之上添加一个init方法。

代码语言:javascript复制
init() {
  NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleAuthState),
    name: .loginStatusChanged,
    object: nil
  )

这样你一旦登录,经过注册的AppController就会通知你已经登录的消息,它会在触发时调用handleAuthState。这样在使用任何电子邮件和密码组合登录后,你都会看到一各好友列表。

你可能会注意到,这些好友没有头像,只有名字。虽然这不太好看,但已经实现了安全登录的目的了,至于美观设计,我会在下面讲到。

你会发现,虽然登录过程很顺利,但却没有办法退出应用程序。这实际上很容易实现,因为会有一个对身份验证状态更改的通知。

返回AuthController.swift并在 signIn(_:password:)下面添加以下内容。

代码语言:javascript复制
class func signOut() throws {
  // 1
  guard let currentUser = Settings.currentUser else {
    return
  }
   
  // 2
  try KeychainPasswordItem(service: serviceName, account: currentUser.email).deleteItem()
   
  // 3
  Settings.currentUser = nil
  NotificationCenter.default.post(name: .loginStatusChanged, object: nil)
}

其主要作用是:

1.检查你是否已经存储了一个当前用户,如果没有,就可以提前退出了;

2.从钥匙串中删除密码哈希;

3.清除用户对象并发布通知;

要连接它,就请跳转到FriendsViewController.swift,并将以下内容添加到当前空的signOut中:

代码语言:javascript复制
try? AuthController.signOut()

当选择注销按钮时,程序就会调用你设置的新方法来清除登录用户的数据。

在应用程序中处理错误是一个好主意, 构建并运行,然后点击注销按钮。

现在你就有了一个在应用程序中使用身份验证的完整示例!

哈希

还记得刚刚说到的朋友列表里只有名字,没有头像的问题吗?现在我就来解决这个问题。

在FriendsViewController.swift中,会显示用户模型对象的列表。要想在朋友列表视图中显示头像,就必须先搞清楚一件事,那就是用户只有两个属性,名称和电子邮件,那你应该如何添加图像呢?

事实证明,有一项服务可以在接受电子邮件地址的同时将该邮件人的头像显示出来,这个服务就是Gravatar。

Gravatar是Globally Recognized Avatar的缩写,是gravatar推出的一项服务,意为“全球通用头像”。如果在Gravatar的服务器上放置了你自己的头像,那么在任何支持Gravatar的blog或者留言本上留言时,只要提供你与这个头像关联的email地址,就能够显示出你的Gravatar头像来。

我们在很多博客或者网站留言,评论的时候会看到有的人头像很酷很个性化,但是这个博客和网站本身并没有提供设置头像的功能,感觉有点神奇,那么是怎么做到的呢?其实这是使用了Gravatar。

Gravatar的概念首先是在国外的独立WordPress博客中兴起的,当你到任何一个支持Gravatar的网站留言时,这个网站都会根据你所提供的Email地址为你显示出匹配的头像。当然,这个头像,是需要你事先到Gravatar的网站注册并上传的,否则,在这个网站上,就只会显示成一个默认的头像。

所以你唯一需要做的就是向Gravatar提出请求并获取他们匹配的头像。为此,你就要创建其电子邮件的MD5哈希以构建请求URL。如果你查看Gravatar网站上的文档,你会发现它需要一个哈希邮件地址来构建用户的请求。由于你可以利用CryptoSwift,这将是小菜一碟。只需在tableView(_:cellForRowAt:)中添加以下代替关于Gravatar的注释即可。

代码语言:javascript复制
// 1
let emailHash = user.email.trimmingCharacters(in: .whitespacesAndNewlines)
                          .lowercased()
                          .md5()
// 2
if let url = URL(string: "https://www.gravatar.com/avatar/"   emailHash) {
  URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, let image = UIImage(data: data) else {
      return
    }
     
    // 3
    self.imageCache.setObject(image, forKey: user.email as NSString)
     
    DispatchQueue.main.async {
      // 4
      self.tableView.reloadRows(at: [indexPath], with: .automatic)
    }
  }.resume()
}

具体进程如下:

1.首先根据Gravatar的文档将电子邮件规范化,然后创建MD5哈希;

2.通过你构建的Gravatar URL和URLSession,从返回的数据中加载UIImage;

3.缓存与头像有关的图像以避免重复获取电子邮件地址;

4.重新加载表格视图中的行,以便显示与头像有关的图像;

构建并运行,现在,你可以查看朋友的头像和名称了。

0 人点赞