api-swift / Sources / IrdAPI / Credentials / KeychainCredentialStore.swift
290 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import Foundation
#if canImport(Security)
import Security

/// Stores the token as a generic password item (this device only, after first unlock).
public struct KeychainCredentialStore: CredentialStore {
    public var service: String
    public var account: String
    public var accessGroup: String?

    public init(service: String = "ro.ird.api.credential", account: String = "default", accessGroup: String? = nil) {
        self.service = service
        self.account = account
        self.accessGroup = accessGroup
    }

    private func query() -> [String: Any] {
        var q: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                kSecAttrService as String: service,
                                kSecAttrAccount as String: account]
        if let accessGroup { q[kSecAttrAccessGroup as String] = accessGroup }
        return q
    }

    public func token() async throws -> String? { try load() }

    /// Synchronous read, for callers that are not async.
    public func load() throws -> String? {
        var q = query()
        q[kSecReturnData as String] = true
        q[kSecMatchLimit as String] = kSecMatchLimitOne
        var item: CFTypeRef?
        let status = SecItemCopyMatching(q as CFDictionary, &item)
        if status == errSecItemNotFound { return nil }
        guard status == errSecSuccess else { throw CredentialStoreError.keychain(status: status) }
        guard let data = item as? Data, let token = String(data: data, encoding: .utf8) else { throw CredentialStoreError.corrupt }
        return token
    }

    public func save(_ token: String) throws {
        let data = Data(token.utf8)
        var add = query()
        add[kSecValueData as String] = data
        add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        let status = SecItemAdd(add as CFDictionary, nil)
        if status == errSecDuplicateItem {
            let update = SecItemUpdate(query() as CFDictionary, [kSecValueData as String: data] as CFDictionary)
            guard update == errSecSuccess else { throw CredentialStoreError.keychain(status: update) }
            return
        }
        guard status == errSecSuccess else { throw CredentialStoreError.keychain(status: status) }
    }

    public func delete() throws {
        let status = SecItemDelete(query() as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else { throw CredentialStoreError.keychain(status: status) }
    }
}
#endif

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git