版本记录
版本号 | 时间 |
---|---|
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安全机制(八)—— 偏好设置的加密存储
9. APP安全机制(九)—— 基本iOS安全之钥匙链和哈希(一)
源码
首先看一下工程项目结构。
1. Swift
1. AppController.swift
import UIKit
final class AppController {
static let shared = AppController()
var window: UIWindow!
var rootViewController: UIViewController? {
didSet {
if let vc = rootViewController {
window.rootViewController = vc
}
}
}
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAuthState),
name: .loginStatusChanged,
object: nil
)
}
func show(in window: UIWindow?) {
guard let window = window else {
fatalError("Cannot layout app with a nil window.")
}
window.backgroundColor = .black
self.window = window
rootViewController = SplashViewController()
window.makeKeyAndVisible()
}
@objc func handleAuthState() {
if AuthController.isSignedIn {
rootViewController = NavigationController(rootViewController: FriendsViewController())
} else {
rootViewController = AuthViewController()
}
}
}
2. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AppController.shared.show(in: UIWindow(frame: UIScreen.main.bounds))
return true
}
}
3. AuthController.swift
import Foundation
import CryptoSwift
final class AuthController {
static let serviceName = "FriendvatarsService"
static var isSignedIn: Bool {
guard let currentUser = Settings.currentUser else {
return false
}
do {
let password = try KeychainPasswordItem(service: serviceName, account: currentUser.email).readPassword()
return password.count > 0
} catch {
return false
}
}
class func passwordHash(from email: String, password: String) -> String {
let salt = "x4vV8bGgqqmQwgCoyXFQj+(o.nUNQhVP7ND"
return "\(password).\(email).\(salt)".sha256()
}
class func signIn(_ user: User, password: String) throws {
let finalHash = passwordHash(from: user.email, password: password)
try KeychainPasswordItem(service: serviceName, account: user.email).savePassword(finalHash)
Settings.currentUser = user
NotificationCenter.default.post(name: .loginStatusChanged, object: nil)
}
class func signOut() throws {
guard let currentUser = Settings.currentUser else {
return
}
try KeychainPasswordItem(service: serviceName, account: currentUser.email).deleteItem()
Settings.currentUser = nil
NotificationCenter.default.post(name: .loginStatusChanged, object: nil)
}
}
extension Notification.Name {
static let loginStatusChanged = Notification.Name("com.razeware.auth.changed")
}
4. DispatchQueue+Delay.swift
import Foundation
extension DispatchQueue {
class func delay(_ delay: Double, closure: @escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + delay,
execute: closure
)
}
}
5. UIColor+Additions.swift
import UIKit
extension UIColor {
static let rwGreen = UIColor(red: 0.0/255.0, green: 104.0/255.0, blue: 55.0/255.0, alpha: 1.0)
6. KeychainPasswordItem.swift
import Foundation
struct KeychainPasswordItem {
// MARK: Types
enum KeychainError: Error {
case noPassword
case unexpectedPasswordData
case unexpectedItemData
case unhandledError(status: OSStatus)
}
// MARK: Properties
let service: String
private(set) var account: String
let accessGroup: String?
// MARK: Intialization
init(service: String, account: String, accessGroup: String? = nil) {
self.service = service
self.account = account
self.accessGroup = accessGroup
}
// MARK: Keychain access
func readPassword() throws -> String {
/*
Build a query to find the item that matches the service, account and
access group.
*/
var query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanTrue
// Try to fetch the existing keychain item that matches the query.
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// Check the return status and throw an error if appropriate.
guard status != errSecItemNotFound else { throw KeychainError.noPassword }
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
// Parse the password string from the query result.
guard let existingItem = queryResult as? [String : AnyObject],
let passwordData = existingItem[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: String.Encoding.utf8)
else {
throw KeychainError.unexpectedPasswordData
}
return password
}
func savePassword(_ password: String) throws {
// Encode the password into an Data object.
let encodedPassword = password.data(using: String.Encoding.utf8)!
do {
// Check for an existing item in the keychain.
try _ = readPassword()
// Update the existing item with the new password.
var attributesToUpdate = [String : AnyObject]()
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject?
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
}
catch KeychainError.noPassword {
/*
No password was found in the keychain. Create a dictionary to save
as a new keychain item.
*/
var newItem = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
newItem[kSecValueData as String] = encodedPassword as AnyObject?
// Add a the new item to the keychain.
let status = SecItemAdd(newItem as CFDictionary, nil)
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
}
}
mutating func renameAccount(_ newAccountName: String) throws {
// Try to update an existing item with the new account name.
var attributesToUpdate = [String : AnyObject]()
attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject?
let query = KeychainPasswordItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup)
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
self.account = newAccountName
}
func deleteItem() throws {
// Delete the existing item from the keychain.
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
let status = SecItemDelete(query as CFDictionary)
// Throw an error if an unexpected status was returned.
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
}
static func passwordItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainPasswordItem] {
// Build a query for all items that match the service and access group.
var query = KeychainPasswordItem.keychainQuery(withService: service, accessGroup: accessGroup)
query[kSecMatchLimit as String] = kSecMatchLimitAll
query[kSecReturnAttributes as String] = kCFBooleanTrue
query[kSecReturnData as String] = kCFBooleanFalse
// Fetch matching items from the keychain.
var queryResult: AnyObject?
let status = withUnsafeMutablePointer(to: &queryResult) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
// If no items were found, return an empty array.
guard status != errSecItemNotFound else { return [] }
// Throw an error if an unexpected status was returned.
guard status == noErr else { throw KeychainError.unhandledError(status: status) }
// Cast the query result to an array of dictionaries.
guard let resultData = queryResult as? [[String : AnyObject]] else { throw KeychainError.unexpectedItemData }
// Create a `KeychainPasswordItem` for each dictionary in the query result.
var passwordItems = [KeychainPasswordItem]()
for result in resultData {
guard let account = result[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedItemData }
let passwordItem = KeychainPasswordItem(service: service, account: account, accessGroup: accessGroup)
passwordItems.append(passwordItem)
}
return passwordItems
}
// MARK: Convenience
private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] {
var query = [String : AnyObject]()
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrService as String] = service as AnyObject?
if let account = account {
query[kSecAttrAccount as String] = account as AnyObject?
}
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject?
}
return query
}
}
7. Settings.swift
import Foundation
final class Settings {
private enum Keys: String {
case user = "current_user"
}
static var currentUser: User? {
get {
guard let data = UserDefaults.standard.data(forKey: Keys.user.rawValue) else {
return nil
}
return try? JSONDecoder().decode(User.self, from: data)
}
set {
if let data = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(data, forKey: Keys.user.rawValue)
} else {
UserDefaults.standard.removeObject(forKey: Keys.user.rawValue)
}
UserDefaults.standard.synchronize()
}
}
}
8. User.swift
import Foundation
struct User: Codable {
let name: String
let email: String
}
9. AuthViewController.swift
import UIKit
final class AuthViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
return true
}
private enum TextFieldTag: Int {
case email
case password
}
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var passwordField: UITextField!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var signInButton: UIButton!
@IBOutlet weak var bottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
containerView.transform = CGAffineTransform(scaleX: 0, y: 0)
containerView.backgroundColor = .rwGreen
containerView.layer.cornerRadius = 7
emailField.delegate = self
emailField.tintColor = .rwGreen
emailField.tag = TextFieldTag.email.rawValue
passwordField.delegate = self
passwordField.tintColor = .rwGreen
passwordField.tag = TextFieldTag.password.rawValue
titleLabel.isHidden = true
view.addGestureRecognizer(
UITapGestureRecognizer(
target: self,
action: #selector(handleTap(_:))
)
)
registerForKeyboardNotifications()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 0.3
animation.fromValue = 0
animation.toValue = 1
CATransaction.begin()
CATransaction.setCompletionBlock {
self.emailField.becomeFirstResponder()
self.titleLabel.isHidden = false
}
containerView.layer.add(animation, forKey: "scale")
containerView.transform = CGAffineTransform(scaleX: 1, y: 1)
CATransaction.commit()
}
// MARK: - Actions
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
view.endEditing(true)
}
@IBAction func signInButtonPressed() {
signIn()
}
// MARK: - Helpers
private func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: NSNotification.Name.UIKeyboardWillShow,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: NSNotification.Name.UIKeyboardWillHide,
object: nil
)
}
private func signIn() {
view.endEditing(true)
guard let email = emailField.text, email.count > 0 else {
return
}
guard let password = passwordField.text, password.count > 0 else {
return
}
let name = UIDevice.current.name
let user = User(name: name, email: email)
do {
try AuthController.signIn(user, password: password)
} catch {
print("Error signing in: \(error.localizedDescription)")
}
}
// MARK: - Notifications
@objc internal func keyboardWillShow(_ notification: Notification) {
guard let userInfo = notification.userInfo else {
return
}
guard let keyboardHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height else {
return
}
guard let keyboardAnimationDuration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
return
}
guard let keyboardAnimationCurve = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue else {
return
}
let options = UIViewAnimationOptions(rawValue: keyboardAnimationCurve << 16)
bottomConstraint.constant = keyboardHeight + 32
UIView.animate(withDuration: keyboardAnimationDuration, delay: 0, options: options, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
@objc internal func keyboardWillHide(_ notification: Notification) {
guard let userInfo = notification.userInfo else {
return
}
guard let keyboardAnimationDuration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
return
}
guard let keyboardAnimationCurve = (userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue else {
return
}
let options = UIViewAnimationOptions(rawValue: keyboardAnimationCurve << 16)
bottomConstraint.constant = 0
UIView.animate(withDuration: keyboardAnimationDuration, delay: 0, options: options, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
extension AuthViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let text = textField.text, text.count > 0 else {
return false
}
switch textField.tag {
case TextFieldTag.email.rawValue:
passwordField.becomeFirstResponder()
case TextFieldTag.password.rawValue:
signIn()
default:
return false
}
return true
}
}
10. FriendsViewController.swift
import UIKit
import CryptoSwift
final class FriendsViewController: UITableViewController {
var friends: [User] = []
var imageCache = NSCache<NSString, UIImage>()
init() {
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Friendvatars"
let reuseIdentifier = String(describing: FriendCell.self)
tableView.register(
UINib(nibName: reuseIdentifier, bundle: nil),
forCellReuseIdentifier: reuseIdentifier
)
navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "Sign Out",
style: .plain,
target: self,
action: #selector(signOut)
)
friends = [
User(name: "Bob Appleseed", email: "ryha26+bob@gmail.com"),
User(name: "Linda Lane", email: "ryha26+linda@gmail.com"),
User(name: "Todd Watch", email: "ryha26+todd@gmail.com"),
User(name: "Mark Towers", email: "ryha26+mark@gmail.com")
]
}
// MARK: - Actions
@objc private func signOut() {
try? AuthController.signOut()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return friends.isEmpty ? 1 : 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 1 : friends.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 64
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return section == 0 ? "Me" : "Friends"
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: FriendCell.self)) as? FriendCell else {
fatalError()
}
let user = indexPath.section == 0 ? Settings.currentUser! : friends[indexPath.row]
cell.nameLabel.text = user.name
if let image = imageCache.object(forKey: user.email as NSString) {
cell.avatarImageView.image = image
} else {
let emailHash = user.email.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.md5()
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
}
self.imageCache.setObject(image, forKey: user.email as NSString)
DispatchQueue.main.async {
self.tableView.reloadRows(at: [indexPath], with: .automatic)
}
}.resume()
}
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
11. NavigationController.swift
import UIKit
final class NavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.tintColor = .white
navigationBar.barTintColor = .rwGreen
navigationBar.prefersLargeTitles = true
navigationBar.titleTextAttributes = [
NSAttributedStringKey.foregroundColor: UIColor.white
]
navigationBar.largeTitleTextAttributes = navigationBar.titleTextAttributes
}
}
12. SplashViewController.swift
import UIKit
final class SplashViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
return true
}
private let backgroundImageView = UIImageView()
private let logoImageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if AuthController.isSignedIn {
AppController.shared.handleAuthState()
} else {
DispatchQueue.delay(1) {
self.animateAndDismiss()
}
}
}
private func setupView() {
backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.image = #imageLiteral(resourceName: "rwdevcon-bg")
logoImageView.translatesAutoresizingMaskIntoConstraints = false
logoImageView.contentMode = .scaleAspectFit
logoImageView.image = #imageLiteral(resourceName: "rw-logo")
view.addSubview(backgroundImageView)
view.addSubview(logoImageView)
NSLayoutConstraint.activate([
backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor),
backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
logoImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
logoImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor),
logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
private func animateAndDismiss() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 0.3
animation.fromValue = 1
animation.toValue = 0
CATransaction.begin()
CATransaction.setCompletionBlock {
AppController.shared.handleAuthState()
}
logoImageView.layer.add(animation, forKey: "scale")
logoImageView.transform = CGAffineTransform(scaleX: 0, y: 0)
CATransaction.commit()
}
}
13. FriendCell.swift
import UIKit
class FriendCell: UITableViewCell {
@IBOutlet var nameLabel: UILabel!
@IBOutlet var avatarImageView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
avatarImageView.clipsToBounds = true
avatarImageView.layer.cornerRadius = avatarImageView.bounds.width / 2
}
override func prepareForReuse() {
super.prepareForReuse()
avatarImageView.image = nil
}
}
后记
本篇主要讲述了基本iOS安全之钥匙链和哈希源码,感兴趣的给个赞或者关注~~~