Skip to content

Instantly share code, notes, and snippets.

@wildthink
Created November 11, 2023 14:08
Show Gist options
  • Save wildthink/b07bdb72ec65d2d8a6c8524b38f2c82d to your computer and use it in GitHub Desktop.
Save wildthink/b07bdb72ec65d2d8a6c8524b38f2c82d to your computer and use it in GitHub Desktop.
Using iCloud for storing keys and then pushing into keychain of device.
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)")
}
}
}
}
/*
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