Created
November 16, 2016 15:18
-
-
Save irace/87f1ec34305023f4b18e42276bc2b986 to your computer and use it in GitHub Desktop.
Swift Keychain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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