APP安全机制(九)—— 基本iOS安全之钥匙链和哈希(一)

版本记录

版本号 时间
V1.0 2018.10.10 星期三

前言

在这个信息爆炸的年代,特别是一些敏感的行业,比如金融业和银行卡相关等等,这都对app的安全机制有更高的需求,很多大公司都有安全 部门,用于检测自己产品的安全性,但是及时是这样,安全问题仍然被不断曝出,接下来几篇我们主要说一下app的安全机制。感兴趣的看我上面几篇。
1. APP安全机制(一)—— 几种和安全性有关的情况
2. APP安全机制(二)—— 使用Reveal查看任意APP的UI
3. APP安全机制(三)—— Base64加密
4. APP安全机制(四)—— MD5加密
5. APP安全机制(五)—— 对称加密
6. APP安全机制(六)—— 非对称加密
7. APP安全机制(七)—— SHA加密
8. APP安全机制(八)—— 偏好设置的加密存储

开始

首先看一下写作环境

Swift 4, iOS 11, Xcode 9

软件开发最重要的一个方面也恰好被认为是最神秘和最可怕的就是应用程序安全性。 用户希望他们的应用程序正确运行,保护其信息的私密性,并保护该信息免受潜在威胁。

在本教程中,您将深入了解iOS安全性的基础知识。 您将使用一些基本的加密hashing方法将用户输入安全地存储在iOS钥匙串中 - 保护您的用户数据的私密性和受保护性。

Apple有几个API可以帮助您保护应用程序的安全,并且您将在使用钥匙串时探索这些API。 此外,您将使用CryptoSwift - 一个经过充分审查的开源库,可实现加密算法。

打开原始工程,看一下示例程序。该示例应用程序允许用户登录并查看他们朋友的照片。 大多数应用程序已经为您连接;你的工作是保护应用程序。

打开Friendvatars.xcworkspace以正确包含所有CocoaPod依赖项。 构建并运行以查看应用程序打开登录屏幕:

目前,点击Sign In时没有任何反应。 这是因为没有办法保存用户的凭据。 这就是你要先添加的内容。


Why Security is Important - 为什么安全如此重要

在深入研究代码之前,您应该了解为什么应用程序中的安全性是必要的。 如果您要存储私人用户数据(如电子邮件,密码或银行帐户信息),则应用程序的安全性尤其重要。

为什么Apple如此重视安全性? 从您拍摄的照片到当天所取得的步数,您的iPhone会存储大量个人数据。 保持这些数据安全非常重要。

谁是iOS生态系统中的攻击者,他们想要什么? 攻击者可能是犯罪分子,商业竞争者,甚至是朋友或亲戚。 并非所有攻击者的目的都一样。 有些人可能想要造成损害或损坏信息,而其他人则可能希望看到他们的生日礼物。

确保应用程序保存的数据免受潜在威胁的侵害是您的职责。 幸运的是,Apple已经构建了许多可以简化此任务的强大API。


Apple’s Keychain - 苹果的钥匙链

iOS Keychain是Apple开发人员最重要的安全元素之一,它是一个用于存储元数据和敏感信息的专用数据库。 使用Keychain是存储对应用程序至关重要的小块数据(如机密和密码)的最佳实践。

为什么使用Keychain而不是更简单的解决方案? 在UserDefaults中存储base-64编码用户密码是不够的? 当然不够! 攻击者恢复以这种方式存储的密码是很轻松的。 安全性很难,尝试自己的自定义解决方案并不是一个好主意。 即使您的应用程序不适用于金融机构,也不应轻易存储私人用户输入。

直接与Keychain交互很复杂,特别是在Swift中。 您必须使用主要用C编写的Security框架。

幸运的是,您可以通过从Apple的示例代码GenericKeychain借用Swift包装器来避免使用这些低级API。 KeychainPasswordItemKeychain提供了一个易于使用的Swift接口,并且已经包含在入门项目中。

是时候深入研究代码!


Using the Keychain - 使用钥匙链

打开AuthViewController.swift。 此视图控制器负责您最初看到的登录表单。 如果向下滚动到Actions部分,您会注意到signInButtonPressed没有执行任何操作。 是时候改变了。 将以下内容添加到Helpers部分的底部:

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) 您可以使用用户输入的电子邮件和密码。 如果其中任何一个是零长度,那么您不想继续。 在实际应用程序中,您还应该在此处向用户显示错误。
  • 3) 您为用户指定了一个名称,出于本教程的目的,您可以从设备名称中获取该名称。

注意:您可以通过转至System Preferences ▸ Sharing and changing the computer’s name来更改Mac的名称(由SIM使用)。 此外,您可以转到Settings ▸ General ▸ About ▸ Name来更改iPhone的名称。

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

signIn()

这会在触发signInButtonPressed时调用signIn方法。

找到textFieldShouldReturn并将case TextFieldTag.password.rawValue替换为break

signIn()

现在,当用户点击键盘上的return而密码field具有焦点并包含文本时,将调用signIn()

signIn()尚未完成。 您仍然需要存储用户对象以及密码。 您将在helper类中实现它。

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

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

static let serviceName = "FriendvatarsService"

这定义了将用于在Keychain中标识应用程序数据的服务名称。 要使用此常量,请在类的末尾创建一个signIn方法,如下所示:

class func signIn(_ user: User, password: String) throws {
  try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(password)
  
  Settings.currentUser = user
}

此方法将用户的登录信息安全地存储在Keychain中。 它创建一个KeychainPasswordItem,其中包含您定义的服务名称以及唯一标识符(account)

对于此应用程序,用户的电子邮件用作Keychain的标识符,但其他示例可以是唯一的用户ID或用户名。 最后,Settings.currentUser设置为user - 这存储在UserDefaults中。

这种方法不应该被认为是完整的! 直接存储用户密码不是最佳做法。 例如,如果攻击者破坏了AppleKeychain,他就可以用纯文本读取您用户的密码。 更好的解决方案是存储根据用户身份构建的哈希。

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

import CryptoSwift

CryptoSwift是用Swift编写的许多标准加密算法中最受欢迎的集合之一。 密码学很难,需要正确完成才能有用。 使用流行的库来实现安全性意味着您无需负责标准化hashing函数的实现。 最好的加密技术向公众开放供审查。

注意:Apple的CommonCrypto框架为您提供了许多有用的hashing函数,但在Swift中与它进行交互并不容易。 这就是为什么在本教程中我们选择了CryptoSwift库。

接下来添加以下signIn

class func passwordHash(from email: String, password: String) -> String {
  let salt = "x4vV8bGgqqmQwgCoyXFQj+(o.nUNQhVP7ND"
  return "\(password).\(email).\(salt)".sha256()
}

此方法接受电子邮件和密码,并返回hashed字符串。 salt是一个唯一的字符串,用于制作通用密码,这种情况并不常见。 sha256()是一个CryptoSwift方法,它在输入字符串上完成一种SHA-2哈希。

在前面的示例中,攻击者破坏了Keychain会发现此哈希值。 攻击者可能会创建一个常用密码表及其哈希值来与此哈希值进行比较。 如果您在没有salting的情况下仅对用户的输入进行了哈希处理,并且密码存在于攻击者哈希表中,则密码将受到损害。

加入盐会增加攻击的复杂性。 此外,您将用户的电子邮件和密码与salt相结合,以创建一个不容易破解的哈希。

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

回到signIn(_:password :),用这个替换调用savePassword的行:

let finalHash = passwordHash(from: user.email, password: password)
try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(finalHash)

signIn现在存储强哈希,而不是原始密码。 现在是时候将它添加到视图控制器了。

返回Auth ViewController.swift并将以下内容添加到signIn()的底部:

do {
  try AuthController.signIn(user, password: password)
} catch {
  print("Error signing in: \(error.localizedDescription)")
}

虽然这将存储user并保存hashed密码,但是应用程序登录需要更多.AppController.swift需要一种在身份验证更改时得到通知的方法。

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

AuthController.swift中,将isSignedIn更新为:

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中的当前用户。 如果没有用户,则没有用于从Keychain查找密码哈希的标识符,因此您表明他们未登录。
  • 2) 您从Keychain读取密码哈希,如果密码存在且不为空,则认为用户已登录。

现在AppController.swift中的handleAuthState可以正常工作,但是在登录后需要新的应用程序启动才能正确更新UI。 相反,通知应用程序状态更改(例如身份验证)的好方法是通过通知。

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

extension Notification.Name {
  
  static let loginStatusChanged = Notification.Name("com.razeware.auth.changed")
  
}

在编写自定义通知时使用反向域标识符是一种很好的做法,通常从应用程序的包标识符派生。 使用唯一标识符可以在调试时提供帮助,因此与您的通知相关的任何内容都会与日志中提到的其他框架完美区别开来。要使用此自定义通知名称,请将以下内容添加到signIn(_:password :)的底部:

NotificationCenter.default.post(name: .loginStatusChanged, object: nil)

这将发布可以由应用程序的其他部分观察到的通知。

AppController.swift里面,在show(in :)上面添加一个init方法:

init() {
  NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleAuthState),
    name: .loginStatusChanged,
    object: nil
  )
}

这将注册AppController作为您的登录通知的观察者。 它会在触发时调用handleAuthState

Build并运行。 使用任何电子邮件和密码组合登录后,您将看到朋友列表:

你会注意到没有任何头像,只有朋友的名字。 这看起来不太令人愉快。 您可能应该退出并忘记这个未完成的应用程序。 哦,来吧,即使退出按钮也不起作用。

登录效果很好,但没有办法退出应用程序。 这实际上很容易实现,因为有一个通知会发出任何身份验证状态更改的信号。

返回到AuthController.swift并在signIn(_:password :)下面添加以下方法:

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) 您检查是否存储了当前用户,如果没有,则提前return
  • 2) 您从Keychain中删除密码哈希。
  • 3) 您清除用户对象并发布通知。

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

try? AuthController.signOut()

调用新方法以在选择Sign Out按钮时清除已登录用户的数据。

处理应用程序中的错误是一个好主意,但是为了本教程,您将忽略任何错误。

Build并运行,然后点击Sign Out按钮。

现在您有一个在应用程序中工作的身份验证的完整示例!


Hashing - 哈希

您在设置身份验证方面做得非常好!然而,乐趣尚未结束。现在,您将在朋友视图中的名称前面找到该空白区域。

FriendsViewController.swift中,显示了一个User模型对象列表。您还想在视图中显示每个用户的头像图像。由于User只有两个属性,一个名称和电子邮件,您应该如何显示图像?

事实证明,有一项服务需要一个电子邮件地址并将其与头像图像联系起来:Gravatar!如果您之前没有听说过Gravatar,它通常用于博客和论坛,以便将电子邮件地址与头像全局关联。这简化了事情,因此用户无需将新头像上传到他们加入的每个论坛或网站。

这些用户中的每一个都具有与其电子邮件相关联的头像。因此,您唯一需要做的就是向Gravatar提出请求并获取他们的图像。为此,您将创建其电子邮件的MD5哈希以构建请求URL。

如果您查看Gravatar’s site上的文档,您会发现需要一个哈希的电子邮件地址来构建请求。这将是一块蛋糕,因为你可以利用CryptoSwift。在tableView(_:cellForRowAt :)中添加以下内容,代替Gravatar的注释:

// 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 URLURLSession。 您从返回的数据加载UIImage
  • 3) 您缓存图像以避免重复提取电子邮件地址。
  • 4) 您在table view中重新加载行,以便显示头像图像。

Build并运行。 现在您可以查看朋友的头像图像和名称:

注意:如果您的电子邮件返回蓝色G上的默认白色,请转到Gravatar的网站并上传您自己的头像并加入您的朋友!

您现在拥有一个完整的应用程序处理基本的iOS安全和身份验证,您可以查看由Gravatar驱动的头像。 您了解了安全性的重要性,关于iOS钥匙串和一些最佳实践,如存储哈希而不是纯文本值。 希望你也很开心学习这个!

后记

本篇主要讲述了基本iOS安全之钥匙链和哈希,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容