diff options
| author | Christian Cleberg <[email protected]> | 2026-03-24 21:48:23 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-24 21:48:23 -0500 |
| commit | cf587aa0573ac4f34e1effe70108a9eca82093ac (patch) | |
| tree | 833e6f4961522e35b08af01b5f012983f8e34678 /Rune/Keychain/KeychainManager.swift | |
| download | rune-1.0.0.tar.gz rune-1.0.0.tar.bz2 rune-1.0.0.zip | |
v1.0v1.0.0
Diffstat (limited to 'Rune/Keychain/KeychainManager.swift')
| -rw-r--r-- | Rune/Keychain/KeychainManager.swift | 91 |
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)" + } + } +} |
