Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use keychain in Swift #934

Open
onmyway133 opened this issue Jul 20, 2023 · 0 comments
Open

How to use keychain in Swift #934

onmyway133 opened this issue Jul 20, 2023 · 0 comments

Comments

@onmyway133
Copy link
Owner

onmyway133 commented Jul 20, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant