版本记录
版本号 | 时间 |
---|---|
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。 KeychainPasswordItem
为Keychain
提供了一个易于使用的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
中。
这种方法不应该被认为是完整的! 直接存储用户密码不是最佳做法。 例如,如果攻击者破坏了Apple
的Keychain
,他就可以用纯文本读取您用户的密码。 更好的解决方案是存储根据用户身份构建的哈希。
在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 URL
和URLSession
。 您从返回的数据加载UIImage
。 - 3) 您缓存图像以避免重复提取电子邮件地址。
- 4) 您在
table view
中重新加载行,以便显示头像图像。
Build并运行。 现在您可以查看朋友的头像图像和名称:
注意:如果您的电子邮件返回蓝色G上的默认白色,请转到
Gravatar
的网站并上传您自己的头像并加入您的朋友!
您现在拥有一个完整的应用程序处理基本的iOS安全和身份验证,您可以查看由Gravatar
驱动的头像。 您了解了安全性的重要性,关于iOS钥匙串和一些最佳实践,如存储哈希而不是纯文本值。 希望你也很开心学习这个!
后记
本篇主要讲述了基本iOS安全之钥匙链和哈希,感兴趣的给个赞或者关注~~~