APP安全机制(二十) —— 基于SwiftUI App的钥匙串服务和生物识别(二)

版本记录

版本号 时间
V1.0 2020.09.07 星期一

前言

在这个信息爆炸的年代,特别是一些敏感的行业,比如金融业和银行卡相关等等,这都对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安全之钥匙链和哈希(一)
10. APP安全机制(十)—— 基本iOS安全之钥匙链和哈希(二)
11. APP安全机制(十一)—— 密码工具:提高用户安全性和体验(一)
12. APP安全机制(十二)—— 密码工具:提高用户安全性和体验(二)
13. APP安全机制(十三)—— 密码工具:提高用户安全性和体验(三)
14. APP安全机制(十四) —— Keychain Services API使用简单示例(一)
15. APP安全机制(十五) —— Keychain Services API使用简单示例(二)
16. APP安全机制(十六) —— Keychain Services API使用简单示例(三)
17. APP安全机制(十七) —— 阻止使用SSL Pinning 和 Alamofire的中间人攻击(一)
18. APP安全机制(十八) —— 阻止使用SSL Pinning 和 Alamofire的中间人攻击(二)
19. APP安全机制(十九) —— 基于SwiftUI App的钥匙串服务和生物识别(一)

开始

1. Swift

首先看下工程组织结构

下面就是源码了

1. TextEditor.swift
import SwiftUI

struct TextEditor: UIViewRepresentable {
  @Binding var text: String

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }

  func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.delegate = context.coordinator

    textView.font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
    textView.isScrollEnabled = true
    textView.isEditable = true
    textView.isUserInteractionEnabled = true
    textView.backgroundColor = UIColor.white

    return textView
  }

  func updateUIView(_ uiView: UITextView, context: Context) {
    uiView.text = text
  }

  class Coordinator: NSObject, UITextViewDelegate {
    var parent: TextEditor

    init(_ textView: TextEditor) {
      self.parent = textView
    }

    func textView(
      _ textView: UITextView,
      shouldChangeTextIn range: NSRange,
      replacementText text: String
    ) -> Bool {
      return true
    }

    func textViewDidChange(_ textView: UITextView) {
      self.parent.text = textView.text
    }
  }
}

struct TextEditor_Previews: PreviewProvider {
  static var previews: some View {
    TextEditor(text: .constant("This is some text."))
  }
}
2. ContentView.swift
import SwiftUI

func randomText(length: Int) -> String {
  let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ       abcdefghijklmnopqrstuvwxyz      "
  return String((0..
3. SetPasswordView.swift
import SwiftUI

struct SetPasswordView: View {
  var title: String
  var subTitle: String
  @State var password1 = ""
  @State var password2 = ""
  @Binding var noteLocked: Bool
  @Binding var showModal: Bool
  @ObservedObject var noteData: NoteData

  var passwordValid: Bool {
    passwordsMatch && !password1.isEmpty
  }

  var passwordsMatch: Bool {
    password1 == password2
  }

  var body: some View {
    VStack(alignment: .leading) {
      Text(title)
        .font(.title)
      Text(subTitle)
        .font(.subheadline)
      SecureField("Password", text: $password1)
        .modifier(PasswordField(error: !passwordsMatch))
      SecureField("Verify Password", text: $password2)
        .modifier(PasswordField(error: !passwordsMatch))
      HStack {
        if password1 != password2 {
          Text("Passwords Do Not Match")
            .padding(.leading)
            .foregroundColor(.red)
        }
        Spacer()
        Button("Set Password") {
          if self.passwordValid {
            self.noteData.updateStoredPassword(self.password1)
            self.noteLocked = false
            self.showModal = false
          }
        }.disabled(!passwordValid)
        .padding()
      }
    }.padding()
  }
}

struct SetPasswordView_Previews: PreviewProvider {
  static var previews: some View {
    SetPasswordView(
      title: "Test",
      subTitle: "This is a test",
      noteLocked: .constant(true),
      showModal: .constant(true),
      noteData: NoteData()
    )
  }
}
4. ToolbarView.swift
import SwiftUI
import LocalAuthentication

func getBiometricType() -> String {
  let context = LAContext()

  _ = context.canEvaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    error: nil)
  switch context.biometryType {
  case .faceID:
    return "faceid"
  case .touchID:
    // In iOS 14 and later, you can use "touchid" here
    return "lock"
  case .none:
    return "lock"
  @unknown default:
    return "lock"
  }
}

// swiftlint:disable multiple_closures_with_trailing_closure
struct ToolbarView: View {
  @Binding var noteLocked: Bool
  @ObservedObject var noteData: NoteData
  @Binding var setPasswordModal: Bool
  @State private var showUnlockModal: Bool = false
  @State private var changePasswordModal: Bool = false

  func tryBiometricAuthentication() {
    // 1
    let context = LAContext()
    var error: NSError?

    // 2
    if context.canEvaluatePolicy(
      .deviceOwnerAuthenticationWithBiometrics,
      error: &error) {
      // 3
      let reason = "Authenticate to unlock your note."
      context.evaluatePolicy(
        .deviceOwnerAuthentication,
        localizedReason: reason) { authenticated, error in
        // 4
        DispatchQueue.main.async {
          if authenticated {
            // 5
            self.noteLocked = false
          } else {
            // 6
            if let errorString = error?.localizedDescription {
              print("Error in biometric policy evaluation: \(errorString)")
            }
            self.showUnlockModal = true
          }
        }
      }
    } else {
      // 7
      if let errorString = error?.localizedDescription {
        print("Error in biometric policy evaluation: \(errorString)")
      }
      showUnlockModal = true
    }
  }

  var body: some View {
    HStack {
      #if DEBUG
      Button(
        action: {
          print("App reset.")
          self.noteData.noteText = ""
          self.noteData.updateStoredPassword("")
        }, label: {
          Image(systemName: "trash")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 25.0, height: 25.0)
        })
      #endif

      Color.clear
        .sheet(isPresented: $setPasswordModal) {
          SetPasswordView(
            title: "Set Note Password",
            subTitle: "Enter a password to protect this note.",
            noteLocked: self.$noteLocked,
            showModal: self.$setPasswordModal,
            noteData: self.noteData
          )
        }

      Spacer()

      Button(
        action: {
          self.changePasswordModal = true
        }) {
        Image(systemName: "arrow.right.arrow.left")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(width: 25.0, height: 25.0)
      }
      .disabled(noteLocked || noteData.isPasswordBlank)
      .sheet(isPresented: $changePasswordModal) {
        SetPasswordView(
          title: "Change Password",
          subTitle: "Enter new password",
          noteLocked: self.$noteLocked,
          showModal: self.$changePasswordModal,
          noteData: self.noteData)
      }

      Button(
        action: {
          if self.noteLocked {
            // Biometric Authentication Point
            self.tryBiometricAuthentication()
          } else {
            self.noteLocked = true
          }
        }) {
        // Lock Icon
        Image(systemName: noteLocked ? getBiometricType() : "lock.open")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(width: 25.0, height: 25.0)
      }
      .sheet(isPresented: $showUnlockModal) {
        if self.noteData.isPasswordBlank {
          SetPasswordView(
            title: "Enter Password",
            subTitle: "Enter a password to protect your notes",
            noteLocked: self.$noteLocked,
            showModal: self.$changePasswordModal,
            noteData: self.noteData)
        } else {
          UnlockView(noteLocked: self.$noteLocked, showModal: self.$showUnlockModal, noteData: self.noteData)
        }
      }
    }
    .frame(height: 64)
  }
}

struct ToolbarView_Previews: PreviewProvider {
  static var previews: some View {
    ToolbarView(noteLocked: .constant(true), noteData: NoteData(), setPasswordModal: .constant(false))
  }
}
5. UnlockView.swift
import SwiftUI

// swiftlint:disable multiple_closures_with_trailing_closure
struct UnlockView: View {
  @State var password = ""
  @State var passwordError = false
  @State var showPassword = false
  @Binding var noteLocked: Bool
  @Binding var showModal: Bool
  @ObservedObject var noteData: NoteData

  var body: some View {
    VStack(alignment: .leading) {
      Text("Enter Password")
        .font(.title)
      Text("Enter password to unlock note")
        .font(.subheadline)
      HStack {
        Group {
          if showPassword {
            TextField("Password", text: $password)
          } else {
            SecureField("Password", text: $password)
          }
        }
        Button(
          action: {
            self.showPassword.toggle()
          }) {
          if showPassword {
            Image(systemName: "eye.slash")
          } else {
            Image(systemName: "eye")
              .padding(.trailing, 5.0)
          }
        }
      }.modifier(PasswordField(error: passwordError))
      HStack {
        if passwordError {
          Text("Incorrect Password")
            .padding(.leading)
            .foregroundColor(.red)
        }
        Spacer()
        Button("Unlock") {
          if !self.noteData.validatePassword(self.password) {
            self.passwordError = true
          } else {
            self.noteLocked = false
            self.showModal = false
          }
        }.padding()
      }
    }.padding()
  }
}

struct ToggleLock_Previews: PreviewProvider {
  static var previews: some View {
    UnlockView(noteLocked: .constant(false), showModal: .constant(true), noteData: NoteData())
  }
}
6. ViewModifiers.swift
import SwiftUI

struct PasswordField: ViewModifier {
  var error: Bool

  func body(content: Content) -> some View {
    content
      .textFieldStyle(RoundedBorderTextFieldStyle())
      .border(error ? Color.red : Color.gray)
  }
}
7. NoteData.swift
import SwiftUI

class NoteData: ObservableObject {
  let textKey = "StoredText"

  @Published var noteText: String {
    didSet {
      UserDefaults.standard.set(noteText, forKey: textKey)
    }
  }

  var isPasswordBlank: Bool {
    getStoredPassword() == ""
  }

  func getStoredPassword() -> String {
    let kcw = KeychainWrapper()
    if let password = try? kcw.getGenericPasswordFor(
      account: "RWQuickNote",
      service: "unlockPassword") {
      return password
    }

    return ""
  }

  func updateStoredPassword(_ password: String) {
    let kcw = KeychainWrapper()
    do {
      try kcw.storeGenericPasswordFor(
        account: "RWQuickNote",
        service: "unlockPassword",
        password: password)
    } catch let error as KeychainWrapperError {
      print("Exception setting password: \(error.message ?? "no message")")
    } catch {
      print("An error occurred setting the password.")
    }
  }

  func validatePassword(_ password: String) -> Bool {
    let currentPassword = getStoredPassword()
    return password == currentPassword
  }

  func changePassword(currentPassword: String, newPassword: String) -> Bool {
    guard validatePassword(currentPassword) == true else { return false }
    updateStoredPassword(newPassword)
    return true
  }

  init() {
    noteText = UserDefaults.standard.string(forKey: textKey) ?? ""
  }
}
8. KeychainServices.swift
import Foundation

struct KeychainWrapperError: Error {
  var message: String?
  var type: KeychainErrorType

  enum KeychainErrorType {
    case badData
    case servicesError
    case itemNotFound
    case unableToConvertToString
  }

  init(status: OSStatus, type: KeychainErrorType) {
    self.type = type
    if let errorMessage = SecCopyErrorMessageString(status, nil) {
      self.message = String(errorMessage)
    } else {
      self.message = "Status Code: \(status)"
    }
  }

  init(type: KeychainErrorType) {
    self.type = type
  }

  init(message: String, type: KeychainErrorType) {
    self.message = message
    self.type = type
  }
}

class KeychainWrapper {
  func storeGenericPasswordFor(
    account: String,
    service: String,
    password: String
  ) throws {
    if password.isEmpty {
      try deleteGenericPasswordFor(account: account, service: service)
      return
    }
    guard let passwordData = password.data(using: .utf8) else {
      print("Error converting value to data.")
      throw KeychainWrapperError(type: .badData)
    }

    // 1
    let query: [String: Any] = [
      // 2
      kSecClass as String: kSecClassGenericPassword,
      // 3
      kSecAttrAccount as String: account,
      // 4
      kSecAttrService as String: service,
      // 5
      kSecValueData as String: passwordData
    ]

    // 1
    let status = SecItemAdd(query as CFDictionary, nil)
    switch status {
    // 2
    case errSecSuccess:
      break
    case errSecDuplicateItem:
      try updateGenericPasswordFor(
        account: account,
        service: service,
        password: password)
    // 3
    default:
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }

  func getGenericPasswordFor(account: String, service: String) throws -> String {
    let query: [String: Any] = [
      // 1
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecAttrService as String: service,
      // 2
      kSecMatchLimit as String: kSecMatchLimitOne,
      kSecReturnAttributes as String: true,
      // 3
      kSecReturnData as String: true
    ]

    var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    guard status != errSecItemNotFound else {
      throw KeychainWrapperError(type: .itemNotFound)
    }
    guard status == errSecSuccess else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }

    guard let existingItem = item as? [String: Any],
      // 2
      let valueData = existingItem[kSecValueData as String] as? Data,
      // 3
      let value = String(data: valueData, encoding: .utf8)
      else {
        // 4
        throw KeychainWrapperError(type: .unableToConvertToString)
    }

    //5
    return value
  }

  func updateGenericPasswordFor(
    account: String,
    service: String,
    password: String
  ) throws {
    guard let passwordData = password.data(using: .utf8) else {
      print("Error converting value to data.")
      return
    }
    // 1
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecAttrService as String: service
    ]

    // 2
    let attributes: [String: Any] = [
      kSecValueData as String: passwordData
    ]

    // 3
    let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    guard status != errSecItemNotFound else {
      throw KeychainWrapperError(message: "Matching Item Not Found", type: .itemNotFound)
    }
    guard status == errSecSuccess else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }

  func deleteGenericPasswordFor(account: String, service: String) throws {
    // 1
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecAttrService as String: service
    ]

    // 2
    let status = SecItemDelete(query as CFDictionary)
    guard status == errSecSuccess || status == errSecItemNotFound else {
      throw KeychainWrapperError(status: status, type: .servicesError)
    }
  }
}

后记

本篇主要讲述了SwiftUI的钥匙串服务和生物识别,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(APP安全机制(二十) —— 基于SwiftUI App的钥匙串服务和生物识别(二))