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}