Skip to content

Instantly share code, notes, and snippets.

@guitaripod
Created June 9, 2021 14:27
Show Gist options
  • Select an option

  • Save guitaripod/37d4d7d28a236711aa0fe2ec0942f4b8 to your computer and use it in GitHub Desktop.

Select an option

Save guitaripod/37d4d7d28a236711aa0fe2ec0942f4b8 to your computer and use it in GitHub Desktop.

Revisions

  1. guitaripod created this gist Jun 9, 2021.
    142 changes: 142 additions & 0 deletions Keychain By Apple
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,142 @@
    import Foundation

    struct KeychainItem {
    // MARK: Types

    enum KeychainError: Error {
    case noPassword
    case unexpectedPasswordData
    case unexpectedItemData
    case unhandledError
    }

    // MARK: Properties

    let service: String

    private(set) var account: String

    let accessGroup: String?

    // MARK: Intialization

    init(service: String, account: String, accessGroup: String? = nil) {
    self.service = service
    self.account = account
    self.accessGroup = accessGroup
    }

    // MARK: Keychain access

    func readItem() throws -> String {
    /*
    Build a query to find the item that matches the service, account and
    access group.
    */
    var query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
    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 }

    // 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 saveItem(_ 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 _ = readItem()

    // Update the existing item with the new password.
    var attributesToUpdate = [String: AnyObject]()
    attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject?

    let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
    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 }
    } catch KeychainError.noPassword {
    /*
    No password was found in the keychain. Create a dictionary to save
    as a new keychain item.
    */
    var newItem = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
    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 }
    }
    }

    func deleteItem() throws {
    // Delete the existing item from the keychain.
    let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
    let status = SecItemDelete(query as CFDictionary)

    // Throw an error if an unexpected status was returned.
    guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError }
    }

    // MARK: Convenience

    private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: 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?
    }

    return query
    }

    /*
    For the purpose of this demo app, the user identifier will be stored in the device keychain.
    You should store the user identifier in your account management system.
    */
    static var currentUserIdentifier: String {
    do {
    let storedIdentifier = try KeychainItem(service: "com.example.apple-samplecode.juice", account: "userIdentifier").readItem()
    return storedIdentifier
    } catch {
    return ""
    }
    }

    static func deleteUserIdentifierFromKeychain() {
    do {
    try KeychainItem(service: "com.example.apple-samplecode.juice", account: "userIdentifier").deleteItem()
    } catch {
    print("Unable to delete userIdentifier from keychain")
    }
    }
    }