Swift Keychain
Small example of how to work with Keychin from Swift
import SwiftUI
struct ContentView: View {
@State private var service: String = ""
@State private var account: String = ""
@State private var secret: String = ""
@State var message: String = ""
@State var show: Bool = false
var body: some View {
Grid {
GridRow {
Text("service")
TextField("service, e.g. https://github.com", text: $service)
}
GridRow {
Text("account")
TextField("account, e.g. john", text: $account)
}
GridRow {
Text("secret")
SecureField("secret, e.g. P@ssword!", text: $secret)
}
}.padding()
VStack {
HStack {
Button("get") {
let (status, secret) = KeychainManager.get(service: service, account: account)
message = String(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)
self.secret = secret ?? ""
if status == errSecSuccess {
message = "Success\n\n\(message)\n\nSecret: \(self.secret)"
} else {
message = "Failure\n\n\(message)"
}
show = true
}.alert(message, isPresented: $show) {
Button("OK") {}
}
Button("insert") {
Task {
let status = await KeychainManager.insert(service: service, account: account, secret: secret)
message = String(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)
if status == errSecSuccess {
message = "Success\n\n\(message)"
} else {
message = "Failure\n\n\(message)"
}
show = true
}
}.alert(message, isPresented: $show) {
Button("OK") { }
}
Button("update") {
Task {
let status = KeychainManager.update(service: service, account: account, secret: secret)
message = String(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)
if status == errSecSuccess {
message = "Success\n\n\(message)"
} else {
message = "Failure\n\n\(message)"
}
show = true
}
}.alert(message, isPresented: $show) {
Button("OK") { }
}
Button("delete") {
Task {
let status = await KeychainManager.delete(service: service, account: account)
message = String(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)
if status == errSecSuccess {
message = "Success\n\n\(message)"
} else {
message = "Failure\n\n\(message)"
}
show = true
}
}.alert(message, isPresented: $show) {
Button("OK") {}
}
Button("save") {
Task {
let status = await KeychainManager.save(service: service, account: account, secret: secret)
message = String(SecCopyErrorMessageString(status, nil) ?? "unknown" as CFString)
if status == errSecSuccess {
message = "Success\n\n\(message)"
} else {
message = "Failure\n\n\(message)"
}
show = true
}
}.alert(message, isPresented: $show) {
Button("OK") {}
}
}
Text("Secrets saved to iCloud Keychain, try fill form, then save, get, update, delete").foregroundStyle(.secondary).padding()
}
}
}
// https://www.youtube.com/watch?v=cQjgBIJtMbw
class KeychainManager {
/// Retrieve secret for given `account` in `service`.
///
/// ```
/// let (status, secret) = KeychainManager.get(service: "https://github.com", account: "john")
/// if status == errSecSuccess && secret != nil {
/// print("secret: \(secret)")
/// }
/// ```
///
/// - Parameters:
/// - service: can by any string, but usually is an url, e.g.: https://github.com/
/// - account: our username, e.g.: john
///
/// - Returns: tuple with `status` and `secret`.
static func get(service: String, account: String) -> (status: OSStatus, secret: String?) {
var result: CFTypeRef?
let status = SecItemCopyMatching([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecReturnData: kCFBooleanTrue as Any,
kSecMatchLimit: kSecMatchLimitOne
] as CFDictionary, &result)
guard let data = result as? Data else {
return (status, nil)
}
let secret = String(decoding: data, as: UTF8.self)
return (status, secret)
}
static func insert(service: String, account: String, secret: String) async -> OSStatus {
guard let data = secret.data(using: .utf8) else {
return errSecBadReq
}
return SecItemAdd([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecValueData: data
] as CFDictionary, nil)
}
static func update(service: String, account: String, secret: String) -> OSStatus {
guard let data = secret.data(using: .utf8) else {
return errSecBadReq
}
return SecItemUpdate([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account
] as CFDictionary, [
kSecValueData: data
] as CFDictionary)
}
/// Deletes secret from keychain
///
/// ```
/// Task {
/// let status = await Keychain.delete(service: "https://github.com/", "john")
/// if status != errSecSuccess {
/// message = String(SecCopyErrorMessageString(status, nil) ?? "unknown error" as CFString)
/// print("unable delete secret because of \(message)")
/// }
/// }
/// ```
///
/// - Parameters:
/// - service: can by any string, but usually is an url, e.g.: https://github.com/
/// - account: our username, e.g.: john
static func delete(service: String, account: String) async -> OSStatus {
return SecItemDelete([
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account
] as CFDictionary)
}
/// Saves secret to keychain
///
/// ```
/// Task {
/// let status = await Keychain.save(service: "https://github.com/", "john", secret: "P@ssword!")
/// if status != errSecSuccess {
/// message = String(SecCopyErrorMessageString(status, nil) ?? "unknown error" as CFString)
/// print("failure: \(message)")
/// }
/// }
/// ```
///
/// - Parameters:
/// - service: can by any string, but usually is an url, e.g.: https://github.com/
/// - account: our username, e.g.: john
/// - secret: password to be saved
static func save(service: String, account: String, secret: String) async -> OSStatus {
let status = update(service: service, account: account, secret: secret)
if status == errSecItemNotFound {
return await insert(service: service, account: account, secret: secret)
}
return status
}
}
#Preview {
ContentView()
}
Notes: strange thing here, somehow Swift complains that SecItemAdd
is an operation that may block main thread and should be done in background to not block UI, but at the same time SecItemUpdate
does not