Created
March 10, 2025 17:14
-
-
Save lzell/45af3f35acf7204388d26867798c3108 to your computer and use it in GitHub Desktop.
Swift helper to store data in keychain
This file contains hidden or 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
// | |
// TrackingKeychain.swift | |
// AIProxy | |
// | |
// Created by Lou Zell on 1/30/25. | |
// | |
import Foundation | |
struct TrackingKeychain { | |
enum Scope { | |
case local(keychainAccount: String) | |
case remote(keychainAccount: String) | |
var keychainAccount: String { | |
switch self { | |
case .local(let keychainAccount): return keychainAccount | |
case .remote(let keychainAccount): return keychainAccount | |
} | |
} | |
} | |
let keychainServiceName = (Bundle.main.bundleIdentifier ?? "com.example") + ".tracking-keychain" | |
let serialQueue = DispatchQueue(label: "tracking-keychain") | |
let secClass: NSCopying | |
let secAttrGeneric: NSCopying | |
let secAttrAccount: NSCopying | |
let secAttrService: NSCopying | |
let secAttrSynchronizable: NSCopying | |
let secMatchLimit: NSCopying | |
let secReturnData: NSCopying | |
let secValueData: NSCopying | |
let cfBooleanTrue: CFBoolean | |
init?() { | |
guard let secClass = kSecClass as? NSCopying, | |
let secAttrGeneric = kSecAttrGeneric as? NSCopying, | |
let secAttrAccount = kSecAttrAccount as? NSCopying, | |
let secAttrService = kSecAttrService as? NSCopying, | |
let secAttrSynchronizable = kSecAttrSynchronizable as? NSCopying, | |
let secMatchLimit = kSecMatchLimit as? NSCopying, | |
let secReturnData = kSecReturnData as? NSCopying, | |
let secValueData = kSecValueData as? NSCopying, | |
let cfBooleanTrue = kCFBooleanTrue | |
else { | |
return nil | |
} | |
self.secClass = secClass | |
self.secAttrGeneric = secAttrGeneric | |
self.secAttrAccount = secAttrAccount | |
self.secAttrService = secAttrService | |
self.secAttrSynchronizable = secAttrSynchronizable | |
self.secMatchLimit = secMatchLimit | |
self.secReturnData = secReturnData | |
self.secValueData = secValueData | |
self.cfBooleanTrue = cfBooleanTrue | |
} | |
func retrieve(scope: Scope) async -> Data? { | |
return await withCheckedContinuation { continuation in | |
self.serialQueue.async { | |
let data = self.searchKeychainCopyMatching(scope: scope) | |
DispatchQueue.main.async { | |
continuation.resume(returning: data) | |
} | |
} | |
} | |
} | |
func create(data: Data, scope: Scope) async -> OSStatus { | |
return await withCheckedContinuation { continuation in | |
self.serialQueue.async { | |
let res = self.createKeychainValue(data: data, scope: scope) | |
DispatchQueue.main.async { | |
continuation.resume(returning: res) | |
} | |
} | |
} | |
} | |
func update(data: Data, scope: Scope) async -> OSStatus { | |
return await withCheckedContinuation { continuation in | |
self.serialQueue.async { | |
let res = self.updateKeychainValue(data: data, scope: scope) | |
DispatchQueue.main.async { | |
continuation.resume(returning: res) | |
} | |
} | |
} | |
} | |
func clear(scope: Scope) { | |
self.serialQueue.async { | |
self.deleteKeychainValue(scope: scope) | |
} | |
} | |
// MARK: - Private | |
private func newSearchDictionary(scope: Scope) -> NSMutableDictionary { | |
let searchDictionary = NSMutableDictionary() | |
searchDictionary.setObject(kSecClassGenericPassword, forKey: self.secClass) | |
searchDictionary.setObject(self.keychainServiceName, forKey: self.secAttrService) | |
searchDictionary.setObject(scope.keychainAccount, forKey: self.secAttrGeneric) | |
searchDictionary.setObject(scope.keychainAccount, forKey: self.secAttrAccount) | |
if case .remote = scope { | |
searchDictionary.setObject(self.cfBooleanTrue, forKey: self.secAttrSynchronizable) | |
} | |
return searchDictionary | |
} | |
private func searchKeychainCopyMatching(scope: Scope) -> Data? { | |
let searchDictionary = self.newSearchDictionary(scope: scope) | |
searchDictionary.setObject(kSecMatchLimitOne, forKey: self.secMatchLimit) | |
searchDictionary.setObject(self.cfBooleanTrue, forKey: self.secReturnData) | |
var queryResult: AnyObject? | |
var status: OSStatus = noErr | |
withUnsafeMutablePointer(to: &queryResult) { | |
status = SecItemCopyMatching(searchDictionary, UnsafeMutablePointer($0)) | |
} | |
if status == errSecItemNotFound { | |
return nil | |
} | |
if status == noErr { | |
return queryResult as? Data | |
} | |
prLogger.error("Unexpected keychain error in searchKeychainCopyMatching: \(status)") | |
return nil | |
} | |
private func createKeychainValue(data: Data, scope: Scope) -> OSStatus { | |
let dictionary = self.newSearchDictionary(scope: scope) | |
dictionary.setObject(data, forKey:self.secValueData) | |
return SecItemAdd(dictionary, nil) | |
} | |
private func updateKeychainValue(data: Data, scope: Scope) -> OSStatus { | |
let searchDictionary = newSearchDictionary(scope: scope) | |
let updateDictionary = NSMutableDictionary() | |
updateDictionary.setObject(data, forKey: self.secValueData) | |
let status: OSStatus = SecItemUpdate(searchDictionary, updateDictionary) | |
if status == errSecItemNotFound { | |
return createKeychainValue(data: data, scope: scope) | |
} | |
return status | |
} | |
private func deleteKeychainValue(scope: Scope) { | |
let searchDictionary = newSearchDictionary(scope: scope) | |
SecItemDelete(searchDictionary) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment