Skip to content

Instantly share code, notes, and snippets.

@irace
Created November 16, 2016 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save irace/87f1ec34305023f4b18e42276bc2b986 to your computer and use it in GitHub Desktop.
Save irace/87f1ec34305023f4b18e42276bc2b986 to your computer and use it in GitHub Desktop.
Swift Keychain
import Foundation
import Result
import Security
/**
* A simple wrapper around the Security framework’s keychain functions, providing a Swifty-er API.
*/
typealias KeychainQuery = [String: Any]
struct Keychain {
/// Key constants to be used in queries
enum Key {
static let securityClass = kSecClass as String
static let attributeLabel = kSecAttrLabel as String
static let serviceName = kSecAttrService as String
static let shouldReturnData = kSecReturnData as String
static let matchLimit = kSecMatchLimit as String
static let data = kSecValueData as String
static let returnData = kSecReturnData as String
}
/// Value constants to be used in queries
enum Value {
static let securityClassGenericPassword = kSecClassGenericPassword as String
static let matchLimitOne = kSecMatchLimitOne as String
}
// MARK: - Public
/**
Insert a new item into the keychain. Will throw a duplicate item error if an item with this name already exists.
- parameter query: A dictionary containing an item class specification and optional entries specifying the item's
attribute values. See the "Attribute Key Constants" section for a description of currently defined attributes.
- returns: A result containing either `Void`, or an error if the object could not be fetched.
*/
static func insert(_ query: KeychainQuery) -> Result<Void, KeychainError> {
var result: AnyObject? = nil
let status = withUnsafeMutablePointer(to: &result) {
SecItemAdd(query as CFDictionary, UnsafeMutablePointer($0))
}
if let error = KeychainError(status: status) {
return .failure(error)
}
else {
return .success()
}
}
/**
Fetch the item that matches the provided query.
- parameter query: A dictionary containing an item class specification and optional attributes for controlling the
search. See the "Keychain Search Attributes" section for a description of currently defined search attributes.
- returns: A result containing either the fetched object, or an error if the object could not be fetched. If the
key exists in the keychain but there is no associated value, we return the same `.ItemNotFound` error that is also
returned if the key itself does not exist.
*/
static func fetch(_ query: KeychainQuery) -> Result<AnyObject, KeychainError> {
var result: AnyObject? = nil
let status = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
}
if let error = KeychainError(status: status) {
return .failure(error)
}
else if let result = result {
return .success(result)
}
else {
// We handle “key exists but value is `nil`” the same as “key does not exist.”
return .failure(.itemNotFound)
}
}
/**
Delete the item that matches the provided query.
- parameter query: A dictionary containing an item class specification and optional attributes for controlling the
search. See the "Attribute Constants" and "Search Constants" sections for a description of currently defined search
attributes.
- returns: A result containing either `Void`, or an error if the object could not be fetched.
*/
static func delete(_ query: KeychainQuery) -> Result<Void, KeychainError> {
if let error = KeychainError(status: SecItemDelete(query as CFDictionary)) {
return .failure(error)
}
else {
return .success()
}
}
}
/**
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: Error {
case functionNotImplemented
case invalidParameters
case memoryAllocationError
case keychainNotAvailable
case duplicateItem
case itemNotFound
case interactionNotAllowed
case decodingError
case authenticationFailed
init?(status: OSStatus) {
switch status {
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
case errSecSuccess:
return nil
case errSecIO:
return nil
case errSecOpWr:
return nil
case errSecParam:
return nil
case errSecUserCanceled:
return nil
case errSecBadReq:
return nil
case errSecInternalComponent:
return nil
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."
}
}
}
import Decodable
public typealias TwoWayCodable = Decodable & Encodable
protocol ObjectStorage {
associatedtype T
func set(_ object: T)
func get() -> T?
func delete()
}
/**
* Provides access to an object stored in the keychain.
*/
public final class KeychainStorage<T: TwoWayCodable>: ObjectStorage {
// MARK: - State
fileprivate let query: KeychainQuery
// MARK: - Initialization
/**
Initialize an accessor for a given object in the keychain.
- parameter key: Key that uniquely identifies the object.
- parameter service: Service where the key is to be found (defaults to a general Prefer service).
- returns: New instance
*/
public init(key: String, service: String = "PreferKeychainService") {
query = [
Keychain.Key.securityClass: Keychain.Value.securityClassGenericPassword,
Keychain.Key.attributeLabel: key,
Keychain.Key.serviceName: service
]
}
// MARK: - Public
/**
Set the underlying object. If the object already exists in the keychain, it will be deleted before the new value is
set.
- parameter object: Object to be set.
*/
public func set(_ object: T) {
delete()
let insertQuery = DictionaryBuilder<String, Any>(dictionary: query)
.add(key: Keychain.Key.data, value: NSKeyedArchiver.archivedData(withRootObject: object.encode()))
.build()
if case .failure(let error) = Keychain.insert(insertQuery) {
log.error(error)
}
}
/**
Retrieve the value from the keychain.
- returns: Value, or `nil` if the value does not exist.
*/
public func get() -> T? {
let result = Keychain.fetch(DictionaryBuilder<String, Any>(dictionary: self.query)
.add(key: Keychain.Key.shouldReturnData, value: true)
.add(key: Keychain.Key.matchLimit, value: Keychain.Value.matchLimitOne)
.build())
do {
switch result {
case .success(let value):
return try (value as? Data).flatMap { data in
return NSKeyedUnarchiver.unarchiveObject(with: data)
}
.flatMap { object in
return try T.decode(object)
}
case .failure(.itemNotFound):
// We don’t log here because it’s not necessarily an error if we check if something exists, and it doesn’t
return nil
case .failure(let error):
log.error(error)
return nil
}
}
catch let error as NSError {
log.error("Unknown error when trying to decode object")
log.recordError(error)
return nil
}
}
/**
Delete the value from the keychain.
*/
public func delete() {
if case .failure(let error) = Keychain.delete(query) {
log.error(error)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment