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安全機制(八)—— 偏好設置的加密存儲
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安全之鑰匙鏈和哈希源碼,感興趣的給個贊或者關注~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容