krz/rune

an ios client for njalla

clone: git clone https://gitbay.org/krz/rune.git

v1.2.0: Rune/Keychain/KeychainManager.swift · raw

 1import Foundation
 2import Security
 3
 4struct KeychainManager {
 5    private let service = "net.cleberg.rune"
 6    private let account = "api-token"
 7
 8    func saveToken(_ token: String) throws {
 9        let encodedToken = Data(token.utf8)
10        var query = baseQuery()
11        query[kSecValueData as String] = encodedToken
12        query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
13
14        let status = SecItemAdd(query as CFDictionary, nil)
15        if status == errSecDuplicateItem {
16            try updateToken(token)
17            return
18        }
19
20        guard status == errSecSuccess else {
21            throw KeychainError.unhandled(status)
22        }
23    }
24
25    func readToken() throws -> String? {
26        var query = baseQuery()
27        query[kSecReturnData as String] = true
28        query[kSecMatchLimit as String] = kSecMatchLimitOne
29
30        var result: AnyObject?
31        let status = SecItemCopyMatching(query as CFDictionary, &result)
32
33        if status == errSecItemNotFound {
34            return nil
35        }
36
37        guard status == errSecSuccess else {
38            throw KeychainError.unhandled(status)
39        }
40
41        guard
42            let data = result as? Data,
43            let token = String(data: data, encoding: .utf8)
44        else {
45            throw KeychainError.invalidData
46        }
47
48        return token
49    }
50
51    func deleteToken() throws {
52        let status = SecItemDelete(baseQuery() as CFDictionary)
53        guard status == errSecSuccess || status == errSecItemNotFound else {
54            throw KeychainError.unhandled(status)
55        }
56    }
57
58    private func updateToken(_ token: String) throws {
59        let attributes: [String: Any] = [
60            kSecValueData as String: Data(token.utf8),
61            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
62        ]
63
64        let status = SecItemUpdate(baseQuery() as CFDictionary, attributes as CFDictionary)
65        guard status == errSecSuccess else {
66            throw KeychainError.unhandled(status)
67        }
68    }
69
70    private func baseQuery() -> [String: Any] {
71        [
72            kSecClass as String: kSecClassGenericPassword,
73            kSecAttrService as String: service,
74            kSecAttrAccount as String: account
75        ]
76    }
77}
78
79enum KeychainError: LocalizedError {
80    case invalidData
81    case unhandled(OSStatus)
82
83    var errorDescription: String? {
84        switch self {
85        case .invalidData:
86            return "Stored token data is invalid."
87        case .unhandled(let status):
88            return SecCopyErrorMessageString(status, nil) as String? ?? "Keychain error: \(status)"
89        }
90    }
91}