美文网首页
APP安全机制(二十) —— 基于SwiftUI App的钥匙串

APP安全机制(二十) —— 基于SwiftUI App的钥匙串

作者: 刀客传奇 | 来源:发表于2020-09-07 09:56 被阅读0次

    版本记录

    版本号 时间
    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..<length).map { _ in letters.randomElement() ?? " " })
    }
    
    struct ContentView: View {
      @ObservedObject var noteData: NoteData
      @State private var noteLocked = true
      @State private var fillerText = randomText(length: 250)
      @State private var setPasswordModal = false
    
      var body: some View {
        VStack(alignment: .leading) {
          Text("RW Quick Note")
            .foregroundColor(Color("rw-green"))
            .font(.largeTitle)
          ToolbarView(noteLocked: $noteLocked, noteData: noteData, setPasswordModal: $setPasswordModal)
            .onAppear {
              if self.noteData.isPasswordBlank {
                self.setPasswordModal = true
              }
            }
          Group {
            if noteLocked {
              TextEditor(text: $fillerText)
                .disabled(true)
                .blur(radius: 5.0)
            } else {
              TextEditor(text: $noteData.noteText)
            }
          }
          .border(Color.gray)
        }
        .padding()
      }
    }
    
    struct ContentView_Previews: PreviewProvider {
      static var previews: some View {
        ContentView(noteData: NoteData())
      }
    }
    
    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的钥匙串

          本文链接:https://www.haomeiwen.com/subject/kcjkektx.html