Created
November 11, 2023 14:08
-
-
Save wildthink/b07bdb72ec65d2d8a6c8524b38f2c82d to your computer and use it in GitHub Desktop.
Using iCloud for storing keys and then pushing into keychain of device.
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
import CloudKit | |
import Foundation | |
import Locksmith | |
/** | |
API Key used by this app is fetched from CloudKit. This class manages fetching and saving it into Keychain | |
*/ | |
class ApiKey { | |
private static let RECORD_NAME = <# RECORD_NAME_IN_CLOUDKIT #> | |
private static let KEY = <# KEY_NAME_IN_CLOUDKIT #> | |
// Keychain labels that are attached to password items | |
enum KeychainLabel: String { | |
case apiKey = <# KEYCHAIN_LABEL #> | |
} | |
// Keychain Configuration | |
struct KeychainConfiguration { | |
static let serviceName = <# IDEALLY_APP_BUNDLE_ID #> | |
static let account = <# APP_NAME #> | |
static let accessGroup: String? = nil | |
static let label: String? = nil | |
} | |
private let _instance = ApiKey() | |
private init() {} | |
/** | |
Retrieves API Key from CloudKit if not already present in Keychain. | |
- Parameters: | |
- completionHandler: calls this closure with API Key | |
*/ | |
static func key(completionHandler: @escaping (String) -> Void) { | |
if let keychainItems = Locksmith.loadDataForUserAccount(userAccount: KeychainConfiguration.account, | |
inService: KeychainConfiguration.serviceName), | |
let key = keychainItems[KeychainLabel.apiKey.rawValue] as? String { | |
completionHandler(key) | |
} else { | |
ApiKey.fetchAndSave { key in | |
completionHandler(key) | |
} | |
} | |
} | |
/** | |
Fetches API Key from CloudKit and saves into Keychain. | |
- Parameters: | |
- completionHandler: calls this closure with API Key | |
*/ | |
static func fetchAndSave(completionHandler: @escaping (String) -> Void) { | |
// Uncomment following block of code and comment the uncommented block below it | |
// in case fetching API Key from Cloud Kit does not work with different dev certificate | |
// Substitue provided key for the variable `key` in code below | |
// do { | |
// // Save the password for the new item. | |
// let key = "" /* PASTE SECRET KEY HERE */ | |
// let keychainItems = [KeychainLabel.apiKey.rawValue: key] | |
// try Locksmith.updateData(data: keychainItems, | |
// forUserAccount: KeychainConfiguration.account, | |
// inService: KeychainConfiguration.serviceName) | |
// | |
// UserDefaults.standard.set(true, forKey: UserDefaults.Keys.apiKeyIsSaved.rawValue) | |
// completionHandler(key) | |
// } catch { | |
// print("Error storing API Key into Keychain: \(error.localizedDescription)") | |
// } | |
// Comment all lines of code below if uncommenting code block above it. | |
let apiKeyRecordID = CKRecord.ID(recordName: ApiKey.RECORD_NAME) | |
let publicDatabase = CKContainer.default().database(with: .public) | |
publicDatabase.fetch(withRecordID: apiKeyRecordID) { record, error in | |
guard let record = record, | |
let key = record[ApiKey.KEY] as? String, | |
!key.isEmpty, | |
error == nil else { | |
print("Error fetching API Key") | |
return | |
} | |
do { | |
// Save the password for the new item. | |
let keychainItems = [KeychainLabel.apiApiKey.rawValue: key] | |
try Locksmith.updateData(data: keychainItems, | |
forUserAccount: KeychainConfiguration.account, | |
inService: KeychainConfiguration.serviceName) | |
UserDefaults.standard.set(true, forKey: UserDefaults.Keys.apiKeyIsSaved.rawValue) | |
completionHandler(key) | |
} catch { | |
print("Error storing API API Key into Keychain: \(error.localizedDescription)") | |
} | |
} | |
} | |
} |
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
/* | |
Copyright (C) 2016 Apple Inc. All Rights Reserved. | |
See LICENSE.txt for this sample’s licensing information | |
Abstract: | |
A struct for accessing generic password keychain items. | |
*/ | |
import Foundation | |
struct KeychainPasswordItem { | |
// MARK: Types | |
enum KeychainError: Error { | |
case noPassword | |
case unexpectedPasswordData | |
case unexpectedItemData | |
case unhandledError(status: OSStatus) | |
} | |
// MARK: Properties | |
let service: String | |
private(set) var account: String | |
let accessGroup: String? | |
let label: String? | |
// MARK: Intialization | |
init(service: String, account: String, accessGroup: String? = nil, label: String? = nil) { | |
self.service = service | |
self.account = account | |
self.accessGroup = accessGroup | |
self.label = label | |
} | |
// MARK: Keychain access | |
func readPassword() throws -> String { | |
/* | |
Build a query to find the item that matches the service, account and | |
access group. | |
*/ | |
var query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup, label: label) | |
query[kSecMatchLimit as String] = kSecMatchLimitOne | |
query[kSecReturnAttributes as String] = kCFBooleanTrue | |
query[kSecReturnData as String] = kCFBooleanTrue | |
// Try to fetch the existing keychain item that matches the query. | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) | |
} | |
// Check the return status and throw an error if appropriate. | |
guard status != errSecItemNotFound else { throw KeychainError.noPassword } | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
// Parse the password string from the query result. | |
guard let existingItem = queryResult as? [String: AnyObject], | |
let passwordData = existingItem[kSecValueData as String] as? Data, | |
let password = String(data: passwordData, encoding: String.Encoding.utf8) | |
else { | |
throw KeychainError.unexpectedPasswordData | |
} | |
return password | |
} | |
func savePassword(_ password: String) throws { | |
// Encode the password into an Data object. | |
let encodedPassword = password.data(using: String.Encoding.utf8)! | |
do { | |
// Check for an existing item in the keychain. | |
try _ = readPassword() | |
// Update the existing item with the new password. | |
var attributesToUpdate = [String: AnyObject]() | |
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? | |
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup, label: label) | |
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
} catch { | |
/* | |
No password was found in the keychain. Create a dictionary to save | |
as a new keychain item. | |
*/ | |
var newItem = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup, label: label) | |
newItem[kSecValueData as String] = encodedPassword as AnyObject? | |
// Add a the new item to the keychain. | |
let status = SecItemAdd(newItem as CFDictionary, nil) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
} | |
} | |
mutating func renameAccount(_ newAccountName: String) throws { | |
// Try to update an existing item with the new account name. | |
var attributesToUpdate = [String: AnyObject]() | |
attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject? | |
let query = KeychainPasswordItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup, label: label) | |
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
self.account = newAccountName | |
} | |
func deleteItem() throws { | |
// Delete the existing item from the keychain. | |
let query = KeychainPasswordItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup, label: label) | |
let status = SecItemDelete(query as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
} | |
static func passwordItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainPasswordItem] { | |
// Build a query for all items that match the service and access group. | |
var query = KeychainPasswordItem.keychainQuery(withService: service, accessGroup: accessGroup) | |
query[kSecMatchLimit as String] = kSecMatchLimitAll | |
query[kSecReturnAttributes as String] = kCFBooleanTrue | |
query[kSecReturnData as String] = kCFBooleanFalse | |
// Fetch matching items from the keychain. | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) | |
} | |
// If no items were found, return an empty array. | |
guard status != errSecItemNotFound else { | |
return [] | |
} | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
// Cast the query result to an array of dictionaries. | |
guard let resultData = queryResult as? [[String: AnyObject]] else { | |
throw KeychainError.unexpectedItemData | |
} | |
// Create a `KeychainPasswordItem` for each dictionary in the query result. | |
var passwordItems = [KeychainPasswordItem]() | |
for result in resultData { | |
guard let account = result[kSecAttrAccount as String] as? String else { | |
throw KeychainError.unexpectedItemData | |
} | |
let passwordItem = KeychainPasswordItem(service: service, account: account, accessGroup: accessGroup) | |
passwordItems.append(passwordItem) | |
} | |
return passwordItems | |
} | |
// MARK: Convenience | |
private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil, label: String? = nil) -> [String: AnyObject] { | |
var query = [String: AnyObject]() | |
query[kSecClass as String] = kSecClassGenericPassword | |
query[kSecAttrService as String] = service as AnyObject? | |
if let account = account { | |
query[kSecAttrAccount as String] = account as AnyObject? | |
} | |
if let accessGroup = accessGroup { | |
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? | |
} | |
if let label = label { | |
query[kSecAttrLabel as String] = label as AnyObject? | |
} | |
return query | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment