版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2018.10.10 星期三 |
前言
在這個信息爆炸的年代,特別是一些敏感的行業(yè),比如金融業(yè)和銀行卡相關(guān)等等,這都對
app
的安全機制有更高的需求,很多大公司都有安全 部門,用于檢測自己產(chǎn)品的安全性,但是及時是這樣,安全問題仍然被不斷曝出,接下來幾篇我們主要說一下app
的安全機制。感興趣的看我上面幾篇。
1. APP安全機制(一)—— 幾種和安全性有關(guān)的情況
2. APP安全機制(二)—— 使用Reveal查看任意APP的UI
3. APP安全機制(三)—— Base64加密
4. APP安全機制(四)—— MD5加密
5. APP安全機制(五)—— 對稱加密
6. APP安全機制(六)—— 非對稱加密
7. APP安全機制(七)—— SHA加密
8. APP安全機制(八)—— 偏好設(shè)置的加密存儲
9. APP安全機制(九)—— 基本iOS安全之鑰匙鏈和哈希(一)
源碼
首先看一下工程項目結(jié)構(gòu)。
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安全之鑰匙鏈和哈希源碼,感興趣的給個贊或者關(guān)注~~~