Skip to content

Instantly share code, notes, and snippets.

@khanlou
Created November 16, 2016 15:16
Show Gist options
  • Save khanlou/4d1736145b496f4e4298cf4dcd08d35e to your computer and use it in GitHub Desktop.
Save khanlou/4d1736145b496f4e4298cf4dcd08d35e to your computer and use it in GitHub Desktop.
import Foundation
import Security
private let DataKey = "MyDataKey"
/**
* The KeychainResult class wraps an OSStatus that is returned from a keychain action. It vends KeychainErrors.
*/
struct KeychainResult {
let keychainError: OSStatus
var isSuccess: Bool {
return keychainError == errSecSuccess
}
//An instance of KeychainError describing the error if there is one.
var error: KeychainError? {
guard !isSuccess else { return nil }
return KeychainError(error: keychainError)
}
}
/**
An enum describing all of the errors that can result from a keychain action.
- FunctionNotImplemented: Function or operation not implemented.
- InvalidParameters: One or more parameters passed to a function were not valid.
- MemoryAllocationError: Failed to allocate memory.
- KeychainNotAvailable: No keychain is available. You may need to restart your computer.
- DuplicateItem: The specified item already exists in the keychain.
- ItemNotFound: The specified item could not be found in the keychain.
- InteractionNotAllowed: User interaction is not allowed.
- DecodingError: Unable to decode the provided data.
- AuthenticationFailed: The user name or passphrase you entered is not correct.
*/
enum KeychainError: ErrorType {
case FunctionNotImplemented
case InvalidParameters
case MemoryAllocationError
case KeychainNotAvailable
case DuplicateItem
case ItemNotFound
case InteractionNotAllowed
case DecodingError
case AuthenticationFailed
init?(error: OSStatus) {
switch error {
case errSecUnimplemented:
self = .FunctionNotImplemented
case errSecParam:
self = .InvalidParameters
case errSecAllocate:
self = .MemoryAllocationError
case errSecNotAvailable:
self = .KeychainNotAvailable
case errSecDuplicateItem:
self = .DuplicateItem
case errSecItemNotFound:
self = .ItemNotFound
case errSecInteractionNotAllowed:
self = .InteractionNotAllowed
case errSecDecode:
self = .DecodingError
case errSecAuthFailed:
self = .AuthenticationFailed
default:
return nil
}
}
/// A description of the error. Not localized.
var errorDescription: String {
switch self {
case .FunctionNotImplemented:
return "Function or operation not implemented."
case .InvalidParameters:
return "One or more parameters passed to a function were not valid."
case .MemoryAllocationError:
return "Failed to allocate memory."
case .KeychainNotAvailable:
return "No keychain is available. You may need to restart your computer."
case .DuplicateItem:
return "The specified item already exists in the keychain."
case .ItemNotFound:
return "The specified item could not be found in the keychain."
case .InteractionNotAllowed:
return "User interaction is not allowed."
case .DecodingError:
return "Unable to decode the provided data."
case .AuthenticationFailed:
return "The user name or passphrase you entered is not correct."
}
}
}
/// A class that conforms to ObjectStorageProtocol and provides access into the keychain.
class KeychainStorage<T: NSCoding>: ObjectStorageProtocol {
typealias KeychainQuery = [String: AnyObject]
let name: String
/**
Constructs a new KeychainStorage. The name of the keychain storage should be unique and will define where the object is stored.
- parameter name: The name of the storage. This name will define where the object is stored.
- returns: An intialized KeychainStorage.
*/
init(name: String) {
self.name = name
}
func saveObject(object: T) {
let attributes: KeychainQuery = [
data: NSKeyedArchiver.archivedDataWithRootObject(object)
]
if let _ = fetchObject() {
updateObject(attributes)
} else {
createNewObject(attributes)
}
}
private func createNewObject(attributes: KeychainQuery) {
var query = newQuery()
query.merge(attributes)
let status = SecItemAdd(query, nil)
let result = KeychainResult(keychainError: status)
if let error = result.error {
print(error)
}
}
private func updateObject(attributes: KeychainQuery) {
let status = SecItemUpdate(newQuery(), attributes)
let result = KeychainResult(keychainError: status)
if let error = result.error {
print(error)
}
}
func fetchObject() -> T? {
var result: AnyObject?
let status = withUnsafeMutablePointer(&result) {
SecItemCopyMatching(newSearchQuery(), UnsafeMutablePointer($0))
}
let keychainResult = KeychainResult(keychainError: status)
if let error = keychainResult.error {
print(error)
}
return result.flatMap({ result in
return result as? NSData
}).flatMap({ data in
return NSKeyedUnarchiver.unarchiveObjectWithData(data)
}).flatMap({ object in
return object as? T
})
}
func deleteObject() {
let status = SecItemDelete(newQuery())
let result = KeychainResult(keychainError: status)
if let error = result.error {
print(error)
}
}
private func newSearchQuery() -> KeychainQuery {
var query = newQuery()
query[shouldReturnData] = true
query[matchLimit] = matchOnlyOne
return query
}
private func newQuery() -> KeychainQuery {
return [
securityClass: kSecClassGenericPassword,
attributeLabel: name,
serviceName: DataKey
]
}
//MARK: - Constants
private let securityClass = kSecClass as String
private let attributeLabel = kSecAttrLabel as String
private let serviceName = kSecAttrService as String
private let shouldReturnData = kSecReturnData as String
private let matchLimit = kSecMatchLimit as String
private let matchOnlyOne = kSecMatchLimitOne as String
private let data = kSecValueData as String
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment