173 lines
6.6 KiB
Swift
173 lines
6.6 KiB
Swift
//
|
|
// Keychain.swift
|
|
// Werkout_ios
|
|
//
|
|
// Created by Trey Tartt on 7/5/23.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
class KeychainInterface {
|
|
enum KeychainError: Error {
|
|
// Attempted read for an item that does not exist.
|
|
case itemNotFound
|
|
|
|
// Attempted save to override an existing item.
|
|
// Use update instead of save to update existing items
|
|
case duplicateItem
|
|
|
|
// A read of an item in any format other than Data
|
|
case invalidItemFormat
|
|
|
|
// Any operation result status than errSecSuccess
|
|
case unexpectedStatus(OSStatus)
|
|
}
|
|
|
|
static let serviceID = "werkout.fitness"
|
|
|
|
static func save(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
|
let query: [String: AnyObject] = [
|
|
// kSecAttrService, kSecAttrAccount, and kSecClass
|
|
// uniquely identify the item to save in Keychain
|
|
kSecAttrService as String: service as AnyObject,
|
|
kSecAttrAccount as String: account as AnyObject,
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
|
|
|
// kSecValueData is the item value to save
|
|
kSecValueData as String: password as AnyObject
|
|
]
|
|
|
|
// SecItemAdd attempts to add the item identified by
|
|
// the query to keychain
|
|
let status = SecItemAdd(
|
|
query as CFDictionary,
|
|
nil
|
|
)
|
|
|
|
// errSecDuplicateItem is a special case where the
|
|
// item identified by the query already exists. Throw
|
|
// duplicateItem so the client can determine whether
|
|
// or not to handle this as an error
|
|
if status == errSecDuplicateItem {
|
|
throw KeychainError.duplicateItem
|
|
}
|
|
|
|
// Any status other than errSecSuccess indicates the
|
|
// save operation failed.
|
|
guard status == errSecSuccess else {
|
|
throw KeychainError.unexpectedStatus(status)
|
|
}
|
|
}
|
|
|
|
static func update(password: Data, service: String = KeychainInterface.serviceID, account: String) throws {
|
|
let query: [String: AnyObject] = [
|
|
// kSecAttrService, kSecAttrAccount, and kSecClass
|
|
// uniquely identify the item to update in Keychain
|
|
kSecAttrService as String: service as AnyObject,
|
|
kSecAttrAccount as String: account as AnyObject,
|
|
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
|
kSecClass as String: kSecClassGenericPassword
|
|
]
|
|
|
|
// attributes is passed to SecItemUpdate with
|
|
// kSecValueData as the updated item value
|
|
let attributes: [String: AnyObject] = [
|
|
kSecValueData as String: password as AnyObject
|
|
]
|
|
|
|
// SecItemUpdate attempts to update the item identified
|
|
// by query, overriding the previous value
|
|
let status = SecItemUpdate(
|
|
query as CFDictionary,
|
|
attributes as CFDictionary
|
|
)
|
|
|
|
// errSecItemNotFound is a special status indicating the
|
|
// item to update does not exist. Throw itemNotFound so
|
|
// the client can determine whether or not to handle
|
|
// this as an error
|
|
guard status != errSecItemNotFound else {
|
|
throw KeychainError.itemNotFound
|
|
}
|
|
|
|
// Any status other than errSecSuccess indicates the
|
|
// update operation failed.
|
|
guard status == errSecSuccess else {
|
|
throw KeychainError.unexpectedStatus(status)
|
|
}
|
|
}
|
|
|
|
static func readPassword(service: String = KeychainInterface.serviceID, account: String) throws -> Data {
|
|
let query: [String: AnyObject] = [
|
|
// kSecAttrService, kSecAttrAccount, and kSecClass
|
|
// uniquely identify the item to read in Keychain
|
|
kSecAttrService as String: service as AnyObject,
|
|
kSecAttrAccount as String: account as AnyObject,
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
|
|
|
// kSecMatchLimitOne indicates keychain should read
|
|
// only the most recent item matching this query
|
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
|
|
// kSecReturnData is set to kCFBooleanTrue in order
|
|
// to retrieve the data for the item
|
|
kSecReturnData as String: kCFBooleanTrue
|
|
]
|
|
|
|
// SecItemCopyMatching will attempt to copy the item
|
|
// identified by query to the reference itemCopy
|
|
var itemCopy: AnyObject?
|
|
let status = SecItemCopyMatching(
|
|
query as CFDictionary,
|
|
&itemCopy
|
|
)
|
|
|
|
// errSecItemNotFound is a special status indicating the
|
|
// read item does not exist. Throw itemNotFound so the
|
|
// client can determine whether or not to handle
|
|
// this case
|
|
guard status != errSecItemNotFound else {
|
|
throw KeychainError.itemNotFound
|
|
}
|
|
|
|
// Any status other than errSecSuccess indicates the
|
|
// read operation failed.
|
|
guard status == errSecSuccess else {
|
|
throw KeychainError.unexpectedStatus(status)
|
|
}
|
|
|
|
// This implementation of KeychainInterface requires all
|
|
// items to be saved and read as Data. Otherwise,
|
|
// invalidItemFormat is thrown
|
|
guard let password = itemCopy as? Data else {
|
|
throw KeychainError.invalidItemFormat
|
|
}
|
|
|
|
return password
|
|
}
|
|
|
|
static func deletePassword(service: String = KeychainInterface.serviceID, account: String) throws {
|
|
let query: [String: AnyObject] = [
|
|
// kSecAttrService, kSecAttrAccount, and kSecClass
|
|
// uniquely identify the item to delete in Keychain
|
|
kSecAttrService as String: service as AnyObject,
|
|
kSecAttrAccount as String: account as AnyObject,
|
|
kSecAttrSynchronizable as String: kCFBooleanTrue,
|
|
kSecClass as String: kSecClassGenericPassword
|
|
]
|
|
|
|
// SecItemDelete attempts to perform a delete operation
|
|
// for the item identified by query. The status indicates
|
|
// if the operation succeeded or failed.
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
|
|
// Any status other than errSecSuccess indicates the
|
|
// delete operation failed.
|
|
guard status == errSecSuccess else {
|
|
throw KeychainError.unexpectedStatus(status)
|
|
}
|
|
}
|
|
}
|