版本记录
版本号 | 时间 |
---|---|
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
的钥匙串服务和生物识别,感兴趣的给个赞或者关注~~~
网友评论