Last active
February 21, 2023 17:19
-
-
Save Fiser12/b92d6ddd71a79840abfd57bd2883ad76 to your computer and use it in GitHub Desktop.
I have created a simplified version of KeychainAccess that allows you to just mark a property of a class like @KeychainWrapper("tag"), to store it under a tag as long as it implements Codable. This makes it very easy to make use of the Keychain.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// I got inspiration in this repository https://github.com/kishikawakatsumi/KeychainAccess | |
// This is a great library that is backward compatible with very old versions of iOS and macOS and gives you a very complete wrapper of the Keychain | |
// But it was too much for me, since I didn't need neither that backward compatibility nor so many features, I just wanted to keep Codable secrets in the Keychain. | |
// So I have made my own version, also implementing a Property Wrapper to make it agnostic to use. | |
// I have thus reduced the 3000 lines of KeychainAccess to less than 200 and I have made its use for the functions I was interested in much more agile and integrated with SwiftUI, | |
// KeychainWrapper.swift | |
// SyncTion (macOS) | |
// | |
// Created by Rubén on 20/2/23. | |
// | |
import Foundation | |
@propertyWrapper | |
struct KeychainWrapper<Wrapped: Codable> { | |
let service: String | |
private var cache: Wrapped? | |
var wrappedValue: Wrapped? { | |
get { | |
self.cache | |
} | |
set { | |
self.cache = newValue | |
self.encode() | |
} | |
} | |
private func encode() { | |
Task { | |
if let data = try? JSONEncoder().encode(self.cache) { | |
try? KeychainAccess.Shared.set(data, key: self.service) | |
} else { | |
try? KeychainAccess.Shared.remove(self.service) | |
} | |
} | |
} | |
mutating func reload() { | |
self.cache = decode | |
} | |
private var decode: Wrapped? { | |
let data = try? KeychainAccess.Shared.getData(service) ?? "nil".data(using: .utf8) | |
guard let data, let wrapped = try? JSONDecoder().decode(Wrapped.self, from: data) else { return nil } | |
return wrapped | |
} | |
static private func getEnvironmentVar(_ name: String) -> String? { | |
guard let rawValue = getenv(name) else { | |
return nil | |
} | |
return String(utf8String: rawValue) | |
} | |
init(_ service: String) { | |
self.service = service | |
self.cache = decode | |
} | |
} | |
public final class KeychainAccess { | |
static let Shared = KeychainAccess(service: Bundle.main.bundleIdentifier!) | |
let service: String | |
public init(service: String) { | |
self.service = service | |
} | |
public func get(_ key: String) throws -> String? { | |
guard let data = try getData(key) else { return nil } | |
guard let string = String(data: data, encoding: .utf8) else { | |
throw KeychainAccessError.conversionError | |
} | |
return string | |
} | |
public func getData(_ key: String) throws -> Data? { | |
let query = self.query(key: key) | |
var result: AnyObject? | |
let status = SecItemCopyMatching(query as CFDictionary, &result) | |
switch status { | |
case errSecSuccess: | |
guard let data = result as? Data else { | |
throw KeychainAccessError.unexpectedError | |
} | |
return data | |
case errSecItemNotFound: | |
return nil | |
default: | |
throw KeychainAccessError.securityError | |
} | |
} | |
public subscript(key: String) -> Data? { | |
get { | |
try? getData(key) | |
} | |
set { | |
guard let value = newValue else { | |
try? remove(key) | |
return | |
} | |
try? set(value, key: key) | |
} | |
} | |
public func set(_ value: String, key: String) throws { | |
guard let data = value.data(using: .utf8, allowLossyConversion: false) else { | |
print("failed to convert string to data") | |
throw KeychainAccessError.conversionError | |
} | |
try set(data, key: key) | |
} | |
public func set(_ value: Data, key: String) throws { | |
let query = self.query(key: key) | |
var status = SecItemCopyMatching(query as CFDictionary, nil) | |
switch status { | |
case errSecSuccess, errSecInteractionNotAllowed: | |
let attributes = self.query(key: key, value: value) | |
status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) | |
if status != errSecSuccess { | |
throw KeychainAccessError.securityError | |
} | |
case errSecItemNotFound: | |
let query = self.query(key: key, value: value) | |
status = SecItemAdd(query as CFDictionary, nil) | |
if status != errSecSuccess { | |
throw KeychainAccessError.securityError | |
} | |
default: | |
throw KeychainAccessError.securityError | |
} | |
} | |
public func remove(_ key: String) throws { | |
let query = self.query(key: key) | |
let status = SecItemDelete(query as CFDictionary) | |
if status != errSecSuccess && status != errSecItemNotFound { | |
throw KeychainAccessError.securityError | |
} | |
} | |
private func query(key: String? = nil, value: Data? = nil) -> [String: Any]{ | |
var attributes = [String: Any]() | |
if let key { | |
attributes[Class] = String(kSecClassGenericPassword) | |
attributes[AttributeService] = service | |
attributes[AttributeAccount] = key | |
} | |
if let value { | |
attributes[ValueData] = value | |
} else { | |
attributes[ReturnData] = kCFBooleanTrue | |
} | |
return attributes | |
} | |
} | |
/** Class Key Constant */ | |
private let Class = String(kSecClass) | |
private let AttributeType = String(kSecAttrType) | |
private let AttributeAccount = String(kSecAttrAccount) | |
private let AttributeService = String(kSecAttrService) | |
private let ReturnData = String(kSecReturnData) | |
private let ValueData = String(kSecValueData) | |
enum KeychainAccessError: Error { | |
case securityError | |
case conversionError | |
case unexpectedError | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment