// // ContentView.swift // KeychainAppPasswordDemo // // Created by Paulo F. Andrade on 2023/05/15. // import SwiftUI import LocalAuthentication // WARNING: MUST CHANGE TEAM_ID to test private let TEAM_ID = "32HNEEH56A" struct ContentView: View { @State var secret: String = "shhhh" @State var password: String = "" @StateObject var item = KeychainItem(identifier: "com.outercorner.KeychainAppPasswordDemo.testItem4") var body: some View { VStack { TextField("Secret", text: $secret) TextField("Password", text: $password) Button("GET", action: getSecureData) Button("SET", action: setSecureData) Button("DELETE", action: delSecureData) } .frame(minWidth: 300) .padding() } private func getSecureData() { do { let context = LAContext() context.setCredential(password.data(using: .utf8), type: .applicationPassword) let secData = try item.get(context: context) let secString = String(data: secData, encoding: .utf8) NSLog("Got \(String(describing: secString))") } catch { NSLog("Failed to get item \(error)") } } private func setSecureData() { do { let context = LAContext() context.setCredential(password.data(using: .utf8), type: .applicationPassword) let secData = secret.data(using: .utf8)! try item.store(data: secData, context: context) } catch { NSLog("Failed to set item \(error)") } } private func delSecureData() { do { try item.delete() } catch { NSLog("Failed to delete item \(error)") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } class KeychainItem: NSObject, ObservableObject { init(identifier: String) { self.identifier = identifier } var identifier: String var exists: Bool { var query = baseQuery query[kSecReturnData] = false query[kSecReturnAttributes] = true let ctx = LAContext() ctx.interactionNotAllowed = true query[kSecUseAuthenticationContext] = ctx return SecItemCopyMatching(query as CFDictionary, nil) != errSecItemNotFound } private var baseQuery: [CFString: Any] { var query: [CFString: Any] = [:] query[kSecClass] = kSecClassGenericPassword query[kSecAttrService] = "com.outercorner.Secrets.Lockbox" query[kSecAttrAccessGroup] = "\(TEAM_ID).com.outercorner.KeychainAppPasswordDemo" query[kSecAttrAccount] = self.identifier query[kSecUseDataProtectionKeychain] = true return query } func store(data: Data, context: LAContext) throws { var query = baseQuery query[kSecValueData] = data query[kSecAttrAccessControl] = try accessControl query[kSecUseAuthenticationContext] = context let status = SecItemAdd(query as CFDictionary, nil) switch status { case errSecSuccess: return case errSecDuplicateItem: throw LockboxErrors.duplicateItem case errSecUserCanceled: throw LockboxErrors.userCanceled default: NSLog("Failed to add item \(status)") throw LockboxErrors.osStatusError(status) } } func delete() throws { let query = baseQuery let status = SecItemDelete(query as CFDictionary) switch status { case errSecSuccess: fallthrough case errSecItemNotFound: return default: NSLog("Unexpected error when deleting key \(status)") throw LockboxErrors.osStatusError(status) } } func get(context: LAContext) throws -> Data { guard context.isCredentialSet(.applicationPassword) else { throw LockboxErrors.passphraseNeeded } var query = baseQuery query[kSecReturnData] = true query[kSecUseAuthenticationContext] = context var returnRef: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &returnRef) switch status { case errSecSuccess: guard let data = returnRef as? Data else { throw LockboxErrors.unexpectedError } return data case errSecItemNotFound: throw LockboxErrors.lockboxEmpty case errSecUserCanceled: throw LockboxErrors.userCanceled case errSecAuthFailed: throw LockboxErrors.authenticationFailed default: NSLog("Failed to fetch item \(status)") throw LockboxErrors.unexpectedError } } private var accessControl: SecAccessControl { get throws { var flags = SecAccessControlCreateFlags() flags.insert(.applicationPassword) var cfError: Unmanaged? guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, &cfError) else { throw cfError.map { LockboxErrors.secError($0.takeRetainedValue()) } ?? LockboxErrors.unexpectedError } return accessControl } } } public enum LockboxErrors: Swift.Error { case unexpectedError case lockboxEmpty case duplicateItem case passphraseNeeded case osStatusError(OSStatus) case secError(CFError) case userCanceled case authenticationFailed case unsupportedAccessMethod }