summaryrefslogtreecommitdiff
path: root/Rune/Keychain/KeychainManager.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Rune/Keychain/KeychainManager.swift')
-rw-r--r--Rune/Keychain/KeychainManager.swift91
1 files changed, 91 insertions, 0 deletions
diff --git a/Rune/Keychain/KeychainManager.swift b/Rune/Keychain/KeychainManager.swift
new file mode 100644
index 0000000..bade4f8
--- /dev/null
+++ b/Rune/Keychain/KeychainManager.swift
@@ -0,0 +1,91 @@
+import Foundation
+import Security
+
+struct KeychainManager {
+ private let service = "net.cleberg.rune"
+ private let account = "api-token"
+
+ func saveToken(_ token: String) throws {
+ let encodedToken = Data(token.utf8)
+ var query = baseQuery()
+ query[kSecValueData as String] = encodedToken
+ query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+
+ let status = SecItemAdd(query as CFDictionary, nil)
+ if status == errSecDuplicateItem {
+ try updateToken(token)
+ return
+ }
+
+ guard status == errSecSuccess else {
+ throw KeychainError.unhandled(status)
+ }
+ }
+
+ func readToken() throws -> String? {
+ var query = baseQuery()
+ query[kSecReturnData as String] = true
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ if status == errSecItemNotFound {
+ return nil
+ }
+
+ guard status == errSecSuccess else {
+ throw KeychainError.unhandled(status)
+ }
+
+ guard
+ let data = result as? Data,
+ let token = String(data: data, encoding: .utf8)
+ else {
+ throw KeychainError.invalidData
+ }
+
+ return token
+ }
+
+ func deleteToken() throws {
+ let status = SecItemDelete(baseQuery() as CFDictionary)
+ guard status == errSecSuccess || status == errSecItemNotFound else {
+ throw KeychainError.unhandled(status)
+ }
+ }
+
+ private func updateToken(_ token: String) throws {
+ let attributes: [String: Any] = [
+ kSecValueData as String: Data(token.utf8),
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
+ ]
+
+ let status = SecItemUpdate(baseQuery() as CFDictionary, attributes as CFDictionary)
+ guard status == errSecSuccess else {
+ throw KeychainError.unhandled(status)
+ }
+ }
+
+ private func baseQuery() -> [String: Any] {
+ [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: account
+ ]
+ }
+}
+
+enum KeychainError: LocalizedError {
+ case invalidData
+ case unhandled(OSStatus)
+
+ var errorDescription: String? {
+ switch self {
+ case .invalidData:
+ return "Stored token data is invalid."
+ case .unhandled(let status):
+ return SecCopyErrorMessageString(status, nil) as String? ?? "Keychain error: \(status)"
+ }
+ }
+}