Issue #934

There are a few keychain wrappers around but for simple needs, you can write it yourself

Here is a basic implementation. I use actor to go with async/await, and a struct KeychainError to contain status code in case we want to deal with error cases.

accessGroup is to define kSecAttrAccessGroup to share keychain across your apps

public actor Keychain {
    public struct KeychainError: Error {
        let status: OSStatus
    }

    let service: String
    let accessGroup: String?
    
    public init(
        service: String,
        accessGroup: String? = nil
    ) {
        self.service = service
        self.accessGroup = accessGroup
    }
}

Since we need some common query parameters across few methods, I usually use helper method. We use kSecClassGenericPassword class so we set key to kSecAttrAccount

func baseQuery(key: String) -> [CFString: Any] {
    var query: [CFString: Any] = [:]
    query[kSecClass] = kSecClassGenericPassword
    query[kSecAttrService] = service
    query[kSecAttrAccount] = key
    if let accessGroup {
        query[kSecAttrAccessGroup] = accessGroup
    }
    
    return query
}

Below is how to get and set Data to keychain

func get(key: String) throws -> Data {
    var query = baseQuery(key: key)
    query[kSecMatchLimit] = kSecMatchLimitOne
    query[kSecReturnAttributes] = kCFBooleanTrue
    query[kSecReturnData] = kCFBooleanTrue
    
    var obj: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &obj)
    
    if status == errSecSuccess,
       let json = obj as? [CFString: AnyObject],
       let data = json[kSecValueData] as? Data {
        return data
    } else {
        throw KeychainError(status: status)
    }
}


func set(key: String, data: Data) throws {
    do {
        _ = try get(key: key)
        try update(key: key, data: data)
    } catch let error as KeychainError {
        if error.status == errSecItemNotFound {
            try add(key: key, data: data)
        }
    }
}

func delete(key: String) throws {
    let query = baseQuery(key: key)

    let status = SecItemDelete(query as CFDictionary)
    if status != errSecSuccess {
        throw KeychainError(status: status)
    }
}

private func update(key: String, data: Data) throws {
    let query = baseQuery(key: key)
    let updates: [CFString: Any] = [
        kSecValueData: data
    ]
    
    let status = SecItemUpdate(query as CFDictionary, updates as CFDictionary)
    if status != errSecSuccess {
        throw KeychainError(status: status)
    }
}

private func add(key: String, data: Data) throws {
    var query = baseQuery(key: key)
    query[kSecValueData] = data
    
    let status = SecItemAdd(query as CFDictionary, nil)
    if status != errSecSuccess {
        throw KeychainError(status: status)
    }
}

If there is no error, then OSStatus will be errSecSuccess which has value 0

There are some other query attributes like