Swift Keychain

Small example of how to work with Keychin from Swift

screenshot

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