Compare commits

..

19 Commits

Author SHA1 Message Date
Matthew Palmer ec6d2f4ee7 Merge pull request #54 from frnde/2.0
Xcode 7 Beta 6 Runtime crash fix
2015-09-04 20:53:09 +10:00
Mounir Dellagi c302757509 Test 2015-09-01 13:52:25 +02:00
Matthew Palmer 267a0559b9 Merge pull request #44 from frnde/2.0
Enabling Testability in main project target to fix errors when compiling
2015-08-05 20:25:39 +10:00
Mounir Dellagi 02f824664a Enabling Testability in main project target to fix errors when compiling 2015-08-03 12:03:19 +02:00
Matthew Palmer 6665a7e16f Merge pull request #43 from jankase/2.0
solve compilation problem for iOS7 targeted apps
2015-08-02 20:08:32 +10:00
Jan Kase 82b46fc760 changed iOS7 fatal error message + make available only for iOS8 WhenPasscodeSetThisDeviceOnly 2015-08-02 09:43:32 +02:00
Jan Kase 6c30f710b2 solve compilation problem for iOS7 targeted apps 2015-08-01 21:09:07 +02:00
matthewpalmer 7a6395f189 Update version number 2015-06-27 15:42:53 +10:00
matthewpalmer c48ce652bd Update README to reflect Swift 2 changes 2015-06-27 15:42:05 +10:00
matthewpalmer 716759b767 Change loadData method to be non-throwing 2015-06-27 15:36:29 +10:00
matthewpalmer 3da0e97cc6 Update tests for new error handling mechanisms 2015-06-27 10:50:20 +10:00
matthewpalmer 03b5015f4f Refactor for Swift 2
* Make more extensive use of enums for Accessible and SecurityClass
  This wraps up the kSecXXX values much more nicely, and makes them
  more easily accessible through the provided enums
2015-06-27 10:46:24 +10:00
matthewpalmer 1b0c092708 Major update for Swift 2
* Add Swift 2-style error handling: many functions now 'throw'
* Add error types conforming to ErrorType, which are thrown
* Consolidate error processing and information sources to be
  in a single enum
* Refactor and clean up logic as a result of these improvements
  in error handling
2015-06-27 10:44:27 +10:00
matthewpalmer 25917eaf15 Add tests for Swift 2 style errors
* Many functions will now throw instead of returning an NSError
* I have run these in a separate demo project, but not as part of
  the main project (Cocoapods/testing woes continue). If you want
  to run these tests standalone, I've put them in a Gist here
  https://gist.github.com/matthewpalmer/17f54354a710828ec193
* Most of these tests use try!, though they could probably be
  improved to use do/try/catch
2015-06-27 10:42:03 +10:00
Matthew Palmer 4dce464fb0 Update README.md
Add README note for installing the 2.0 branch
2015-06-23 07:38:07 +10:00
matthewpalmer 5e508a7551 Merge branch 'SirWellington-feature/swift-2.0-update' into 2.0
* SirWellington-feature/swift-2.0-update:
  + Updates to the new Swift 2 syntax
2015-06-23 07:32:39 +10:00
matthewpalmer a2e58ce87f Merge branch 'feature/swift-2.0-update' of https://github.com/SirWellington/Locksmith into SirWellington-feature/swift-2.0-update
* 'feature/swift-2.0-update' of https://github.com/SirWellington/Locksmith:
  + Updates to the new Swift 2 syntax
2015-06-23 07:32:29 +10:00
Juan Wellington Moreno 825d7d2485 + Updates to the new Swift 2 syntax 2015-06-20 14:22:43 -07:00
matthewpalmer 0418e37185 Release 1.2.2 2015-05-24 09:44:45 +10:00
9 changed files with 485 additions and 374 deletions
@@ -203,6 +203,7 @@
isa = PBXProject;
attributes = {
CLASSPREFIX = Locksmith;
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0510;
ORGANIZATIONNAME = matthewpalmer;
TargetAttributes = {
+1
View File
@@ -984,6 +984,7 @@
50F3A9DFDD0E9D9B2A0EC128 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0510;
};
buildConfigurationList = 9B6594FB78E82DA91E0F5B4C /* Build configuration list for PBXProject "Pods" */;
+49 -61
View File
@@ -2,23 +2,18 @@
// LocksmithTests.swift
// LocksmithTests
//
// Copyright (c) 2014 Mathew Palmer. All rights reserved.
// Created by Matthew Palmer on 27/06/2015.
// Copyright © 2015 Matthew Palmer. All rights reserved.
//
import UIKit
import XCTest
import Locksmith
let myService = "myService"
let sampleData = ["key": "value"]
let myUserAccount = "myUserAccount"
class LocksmithTests: XCTestCase {
class LocksmithDemoTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
Locksmith.clearKeychain()
try! Locksmith.clearKeychain()
}
override func tearDown() {
@@ -26,99 +21,92 @@ class LocksmithTests: XCTestCase {
super.tearDown()
}
// public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
func testSaveData_Once() {
let error = Locksmith.saveData(["key": "value"], forUserAccount: myUserAccount, inService: myService)
XCTAssert(error == nil, "❌: saving data")
try! Locksmith.saveData(["key": "value"], forUserAccount: "myUserAccount", inService: "myService")
// XCTAssert(error == nil, ": saving data")
}
func testSaveData_Multiple() {
var errors: [NSError?] = []
// var errors: [NSError?] = []
for i in 0...10 {
errors.append(Locksmith.saveData(["key": "value \(i)"], forUserAccount: "myAccount\(i)", inService: "myService"))
try! Locksmith.saveData(["key": "value \(i)"], forUserAccount: "myAccount\(i)", inService: "myService")
}
XCTAssert(errors.filter({ $0 != nil }).isEmpty, "❌: saving multiple items")
// XCTAssert(errors.filter({ $0 != nil }).isEmpty, ": saving multiple items")
}
func testSaveData_Duplicate() {
// Should be successful
let error1 = Locksmith.saveData(sampleData, forUserAccount: "user", inService: myService)
try! Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
// Should fail
let error2 = Locksmith.saveData(sampleData, forUserAccount: "user", inService: "myService")
XCTAssert(error1 == nil && error2 != nil, "❌: saving duplicate data")
}
// Test using default values for the service
func testWorkflow_Defaults() {
let error1 = Locksmith.saveData(sampleData, forUserAccount: "me")
let error2 = Locksmith.saveData(sampleData, forUserAccount: "me2")
XCTAssert(error1 == nil && error2 == nil, "❌: saving with default service")
let (dict1, err1) = Locksmith.loadDataForUserAccount("me")
let (dict2, err2) = Locksmith.loadDataForUserAccount("me2")
XCTAssert(dict1 != nil && dict2 != nil && err1 == nil && err2 == nil, "❌: loading with default service")
do {
try Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
} catch {
XCTAssert(true)
}
}
// Setup the keychain for requests that use pre-existing values on the keychain (update, read, delete)
func setupLoads() {
Locksmith.saveData(["key": "value"], forUserAccount: "user1", inService: "myService")
Locksmith.saveData(["anotherkey": "anothervalue"], forUserAccount: "user2", inService: "myService")
Locksmith.saveData(["word": "definition"], forUserAccount: "user3", inService: "myService")
try! Locksmith.saveData(["key": "value"], forUserAccount: "user1", inService: "myService")
try! Locksmith.saveData(["anotherkey": "anothervalue"], forUserAccount: "user2", inService: "myService")
try! Locksmith.saveData(["word": "definition"], forUserAccount: "user3", inService: "myService")
}
// public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?)
func testLoadData_Once() {
setupLoads()
let (dictionary, error) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key")! as! NSString == "value" && error == nil, "❌: loading one item")
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value", "❌: loading one item")
}
func testLoadData_Multiple() {
setupLoads()
let (dictionary, error) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
let (dictionary2, error2) = Locksmith.loadDataForUserAccount("user2", inService: "myService")
let (dictionary3, error3) = Locksmith.loadDataForUserAccount("user3", inService: "myService")
XCTAssert(dictionary!.valueForKey("key")! as! NSString == "value" && error == nil, "❌: loading multiple items")
XCTAssert(dictionary2!.valueForKey("anotherkey")! as! NSString == "anothervalue" && error == nil, "❌: loading multiple items")
XCTAssert(dictionary3!.valueForKey("word")! as! NSString == "definition" && error == nil, "❌: loading multiple items")
do {
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
let dictionary2 = Locksmith.loadDataForUserAccount("user2", inService: "myService")
let dictionary3 = Locksmith.loadDataForUserAccount("user3", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value", "❌: loading multiple items")
XCTAssert(dictionary2!.valueForKey("anotherkey") as! NSString == "anothervalue", "❌: loading multiple items")
XCTAssert(dictionary3!.valueForKey("word") as! NSString == "definition", "❌: loading multiple items")
} catch {
XCTAssert(false)
}
}
// public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
func testUpdateData() {
setupLoads()
let error = Locksmith.updateData(["key": "newvalue"], forUserAccount: "user1", inService: "myService")
let (dictionary, err) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key")! as! NSString == "newvalue" && error == nil, "❌: updating item")
try! Locksmith.updateData(["key": "newvalue"], forUserAccount: "user1", inService: "myService")
let dictionary = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "newvalue", "❌: updating item")
// Updating an item that doesn't exist should create that item (i.e. performs a regular create request)
let error2 = Locksmith.updateData(["key": "anothervalue"], forUserAccount: "user1", inService: "myService")
XCTAssert(error2 == nil, "❌: updating item that doesn't exist")
try! Locksmith.updateData(["key": "anothervalue"], forUserAccount: "user1", inService: "myService")
XCTAssert(true, "❌: updating item that doesn't exist")
}
// public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError?
func testDeleteData() {
setupLoads()
let error = Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(error == nil, "❌: deleting existing item")
try! Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(true, "❌: deleting existing item")
let error2 = Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(error2 != nil, "❌: deleting non existent item")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
do {
// Should fail
try Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(false, "❌: did not throw error on deleting non existent item")
} catch {
XCTAssert(true, "❌: deleting non existent item")
}
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
Pod::Spec.new do |s|
s.name = "Locksmith"
s.version = "1.2.3"
s.version = "2.0"
s.summary = "Locksmith is a sane way to work with the iOS Keychain in Swift."
s.description = <<-DESC
Locksmith is a sane way to work with the iOS Keychain in Swift.
+3
View File
@@ -162,6 +162,7 @@
BFFB19C71A4870A300CCFFC3 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0610;
ORGANIZATIONNAME = "Mathew Palmer";
TargetAttributes = {
@@ -332,6 +333,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_TESTABILITY = YES;
INFOPLIST_FILE = "$(SRCROOT)/Pod/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -349,6 +351,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_TESTABILITY = YES;
INFOPLIST_FILE = "$(SRCROOT)/Pod/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+215 -81
View File
@@ -1,109 +1,243 @@
//
// LocksmithTests.swift
// LocksmithTests
// Locksmith.swift
//
// Created by Michael Hahn on 12/22/14.
// Copyright (c) 2014 Mathew Palmer. All rights reserved.
// Created by Matthew Palmer on 26/10/2014.
// Copyright (c) 2014 Colour Coding. All rights reserved.
//
@testable import Locksmith
import CoreFoundation
import UIKit
import XCTest
import Locksmith
import Security
class LocksmithTests: XCTestCase {
public let LocksmithDefaultService = NSBundle.mainBundle().infoDictionary![String(kCFBundleIdentifierKey)] as? String ?? "com.locksmith.defaultService"
// MARK: Locksmith Error
public enum LocksmithError: String, ErrorType {
case Allocate = "Failed to allocate memory."
case AuthFailed = "Authorization/Authentication failed."
case Decode = "Unable to decode the provided data."
case Duplicate = "The item already exists."
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
case NoError = "No error."
case NotAvailable = "No trust results are available."
case NotFound = "The item cannot be found."
case Param = "One or more parameters passed to the function were not valid."
case RequestNotSet = "The request was not set"
case TypeNotFound = "The type was not found"
case UnableToClear = "Unable to clear the keychain"
case Undefined = "An undefined error occurred"
case Unimplemented = "Function or operation not implemented."
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
Locksmith.clearKeychain()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
// public class func saveData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
func testSaveData_Once() {
let error = Locksmith.saveData(["key": "value"], forUserAccount: "myUserAccount", inService: "myService")
XCTAssert(error == nil, "❌: saving data")
}
func testSaveData_Multiple() {
var errors: [NSError?] = []
for i in 0...10 {
errors.append(Locksmith.saveData(["key": "value \(i)"], forUserAccount: "myAccount\(i)", inService: "myService"))
init?(fromStatusCode code: Int) {
switch code {
case Int(errSecAllocate):
self = Allocate
case Int(errSecAuthFailed):
self = AuthFailed
case Int(errSecDecode):
self = Decode
case Int(errSecDuplicateItem):
self = Duplicate
case Int(errSecInteractionNotAllowed):
self = InteractionNotAllowed
case Int(errSecItemNotFound):
self = NotFound
case Int(errSecNotAvailable):
self = NotAvailable
case Int(errSecParam):
self = Param
case Int(errSecUnimplemented):
self = Unimplemented
default:
return nil
}
XCTAssert(errors.filter({ $0 != nil }).isEmpty, "❌: saving multiple items")
}
}
// MARK: Locksmith
public class Locksmith: NSObject {
// MARK: Perform request
public class func performRequest(request: LocksmithRequest) throws -> NSDictionary {
let type = request.type
var result: AnyObject?
var status: OSStatus?
let parsedRequest: NSMutableDictionary = parseRequest(request)
let requestReference = parsedRequest as CFDictionaryRef
switch type {
case .Create:
status = withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
case .Read:
status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
case .Delete:
status = SecItemDelete(requestReference)
case .Update:
status = Locksmith.performUpdate(requestReference, result: &result)
}
guard let unwrappedStatus = status else {
throw LocksmithError.TypeNotFound
}
let statusCode = Int(unwrappedStatus)
if let error = LocksmithError(fromStatusCode: statusCode) {
throw error
}
var resultsDictionary: NSDictionary?
if result != nil && type == .Read && status == errSecSuccess {
if let data = result as? NSData {
// Convert the retrieved data to a dictionary
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
}
}
return resultsDictionary ?? NSDictionary()
}
func testSaveData_Duplicate() {
// Should be successful
let error1 = Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
// MARK: Private methods
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
SecItemDelete(request)
// Should fail
let error2 = Locksmith.saveData(["key": "value"], forUserAccount: "user", inService: "myService")
XCTAssert(error1 == nil && error2 != nil, "❌: saving duplicate data")
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
// If we get an error saving, we'll tell the user about it.
let status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
return status
}
// Setup the keychain for requests that use pre-existing values on the keychain (update, read, delete)
func setupLoads() {
Locksmith.saveData(["key": "value"], forUserAccount: "user1", inService: "myService")
Locksmith.saveData(["anotherkey": "anothervalue"], forUserAccount: "user2", inService: "myService")
Locksmith.saveData(["word": "definition"], forUserAccount: "user3", inService: "myService")
private class func parseRequest(request: LocksmithRequest) -> NSMutableDictionary {
var parsedRequest = NSMutableDictionary()
var options = [String: AnyObject?]()
options[String(kSecAttrAccount)] = request.userAccount
options[String(kSecAttrAccessGroup)] = request.group
options[String(kSecAttrService)] = request.service
options[String(kSecAttrSynchronizable)] = request.synchronizable
options[String(kSecClass)] = request.securityClass.rawValue
if let accessibleMode = request.accessible {
options[String(kSecAttrAccessible)] = accessibleMode.rawValue
}
for (key, option) in options {
parsedRequest.setOptional(option, forKey: key)
}
switch request.type {
case .Create:
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
case .Delete:
parsedRequest = parseDeleteRequest(request, inDictionary: parsedRequest)
case .Update:
parsedRequest = parseCreateRequest(request, inDictionary: parsedRequest)
default: // case .Read:
parsedRequest = parseReadRequest(request, inDictionary: parsedRequest)
}
return parsedRequest
}
// public class func loadDataInService(service: String, forUserAccount userAccount: String) -> (NSDictionary?, NSError?)
func testLoadData_Once() {
setupLoads()
private class func parseCreateRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
let (dictionary, error) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value" && error == nil, "❌: loading one item")
if let data = request.data {
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(data)
dictionary.setObject(encodedData, forKey: String(kSecValueData))
}
return dictionary
}
func testLoadData_Multiple() {
setupLoads()
private class func parseReadRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
dictionary.setOptional(kCFBooleanTrue, forKey: String(kSecReturnData))
let (dictionary, error) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
let (dictionary2, _) = Locksmith.loadDataForUserAccount("user2", inService: "myService")
let (dictionary3, _) = Locksmith.loadDataForUserAccount("user3", inService: "myService")
switch request.matchLimit {
case .One:
dictionary.setObject(kSecMatchLimitOne, forKey: String(kSecMatchLimit))
case .Many:
dictionary.setObject(kSecMatchLimitAll, forKey: String(kSecMatchLimit))
}
XCTAssert(dictionary!.valueForKey("key") as! NSString == "value" && error == nil, "❌: loading multiple items")
XCTAssert(dictionary2!.valueForKey("anotherkey") as! NSString == "anothervalue" && error == nil, "❌: loading multiple items")
XCTAssert(dictionary3!.valueForKey("word") as! NSString == "definition" && error == nil, "❌: loading multiple items")
return dictionary
}
// public class func updateData(data: Dictionary<String, String>, inService service: String, forUserAccount userAccount: String) -> NSError?
func testUpdateData() {
setupLoads()
let error = Locksmith.updateData(["key": "newvalue"], forUserAccount: "user1", inService: "myService")
let (dictionary, _) = Locksmith.loadDataForUserAccount("user1", inService: "myService")
XCTAssert(dictionary!.valueForKey("key") as! NSString == "newvalue" && error == nil, "❌: updating item")
// Updating an item that doesn't exist should create that item (i.e. performs a regular create request)
let error2 = Locksmith.updateData(["key": "anothervalue"], forUserAccount: "user1", inService: "myService")
XCTAssert(error2 == nil, "❌: updating item that doesn't exist")
private class func parseDeleteRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
return dictionary
}
}
// MARK: Convenient Class Methods
extension Locksmith {
public class func saveData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
let saveRequest = LocksmithRequest(userAccount: userAccount, requestType: .Create, data: data, service: service)
try Locksmith.performRequest(saveRequest)
}
// public class func deleteDataInService(service: String, forUserAccount userAccount: String) -> NSError?
func testDeleteData() {
setupLoads()
public class func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> NSDictionary? {
let readRequest = LocksmithRequest(userAccount: userAccount, service: service)
let error = Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(error == nil, "❌: deleting existing item")
let error2 = Locksmith.deleteDataForUserAccount("user1", inService: "myService")
XCTAssert(error2 != nil, "❌: deleting non existent item")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
do {
let dictionary = try Locksmith.performRequest(readRequest)
return dictionary
} catch {
return nil
}
}
}
public class func deleteDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) throws {
let deleteRequest = LocksmithRequest(userAccount: userAccount, requestType: .Delete, service: service)
try Locksmith.performRequest(deleteRequest)
}
public class func updateData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
let updateRequest = LocksmithRequest(userAccount: userAccount, requestType: .Update, data: data, service: service)
try Locksmith.performRequest(updateRequest)
}
public class func clearKeychain() throws {
// Delete all of the keychain data of the given class
func deleteDataForSecClass(secClass: CFTypeRef) throws {
let request = NSMutableDictionary()
request.setObject(secClass, forKey: String(kSecClass))
let status: OSStatus? = SecItemDelete(request as CFDictionaryRef)
if let status = status {
let statusCode = Int(status)
if let error = LocksmithError(fromStatusCode: statusCode) {
throw error
}
}
}
// For each of the sec class types, delete all of the saved items of that type
let classes = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
for classType in classes {
do {
try deleteDataForSecClass(classType)
} catch let error as LocksmithError {
// There was an error
// If the error indicates that there was no item with that security class, that's fine.
// Some of the sec classes will have nothing in them in most cases.
if error != LocksmithError.NotFound {
throw LocksmithError.UnableToClear
}
}
}
}
}
// MARK: Dictionary Extension
extension NSMutableDictionary {
func setOptional(optional: AnyObject?, forKey key: NSCopying) {
if let object: AnyObject = optional {
self.setObject(object, forKey: key)
}
}
}
+103 -184
View File
@@ -5,18 +5,60 @@
// Copyright (c) 2014 Colour Coding. All rights reserved.
//
import CoreFoundation
import UIKit
import Security
public let LocksmithErrorDomain = "com.locksmith.error"
public let LocksmithDefaultService = NSBundle.mainBundle().bundleIdentifier ?? "com.locksmith.defaultService"
public let LocksmithDefaultService = NSBundle.mainBundle().infoDictionary![String(kCFBundleIdentifierKey)] as? String ?? "com.locksmith.defaultService"
// MARK: Locksmith Error
public enum LocksmithError: String, ErrorType {
case Allocate = "Failed to allocate memory."
case AuthFailed = "Authorization/Authentication failed."
case Decode = "Unable to decode the provided data."
case Duplicate = "The item already exists."
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
case NoError = "No error."
case NotAvailable = "No trust results are available."
case NotFound = "The item cannot be found."
case Param = "One or more parameters passed to the function were not valid."
case RequestNotSet = "The request was not set"
case TypeNotFound = "The type was not found"
case UnableToClear = "Unable to clear the keychain"
case Undefined = "An undefined error occurred"
case Unimplemented = "Function or operation not implemented."
init?(fromStatusCode code: Int) {
switch code {
case Int(errSecAllocate):
self = Allocate
case Int(errSecAuthFailed):
self = AuthFailed
case Int(errSecDecode):
self = Decode
case Int(errSecDuplicateItem):
self = Duplicate
case Int(errSecInteractionNotAllowed):
self = InteractionNotAllowed
case Int(errSecItemNotFound):
self = NotFound
case Int(errSecNotAvailable):
self = NotAvailable
case Int(errSecParam):
self = Param
case Int(errSecUnimplemented):
self = Unimplemented
default:
return nil
}
}
}
// MARK: Locksmith
public class Locksmith: NSObject {
// MARK: Perform request
public class func performRequest(request: LocksmithRequest) -> (NSDictionary?, NSError?) {
public class func performRequest(request: LocksmithRequest) throws -> NSDictionary? {
let type = request.type
//var result: Unmanaged<AnyObject>? = nil
var result: AnyObject?
var status: OSStatus?
@@ -26,97 +68,45 @@ public class Locksmith: NSObject {
switch type {
case .Create:
status = withUnsafeMutablePointer(&result) { SecItemAdd(requestReference, UnsafeMutablePointer($0)) }
status = SecItemAdd(requestReference, &result)
case .Read:
status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(requestReference, UnsafeMutablePointer($0)) }
status = SecItemCopyMatching(requestReference, &result)
case .Delete:
status = SecItemDelete(requestReference)
case .Update:
status = Locksmith.performUpdate(requestReference, result: &result)
}
if let status = status {
let statusCode = Int(status)
let error = Locksmith.keychainError(forCode: statusCode)
var resultsDictionary: NSDictionary?
if result != nil {
if type == .Read && status == errSecSuccess {
if let data = result as? NSData {
// Convert the retrieved data to a dictionary
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
}
}
guard let unwrappedStatus = status else {
throw LocksmithError.TypeNotFound
}
let statusCode = Int(unwrappedStatus)
if let error = LocksmithError(fromStatusCode: statusCode) {
throw error
}
var resultsDictionary: NSDictionary?
if result != nil && type == .Read && status == errSecSuccess {
if let data = result as? NSData {
// Convert the retrieved data to a dictionary
resultsDictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
}
return (resultsDictionary, error)
} else {
let code = LocksmithErrorCode.TypeNotFound.rawValue
let message = internalErrorMessage(forCode: code)
return (nil, NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message]))
}
}
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
SecItemDelete(request)
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
// If we get an error saving, we'll tell the user about it.
let status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
return status
}
// MARK: Error Lookup
enum ErrorMessage: String {
case Allocate = "Failed to allocate memory."
case AuthFailed = "Authorization/Authentication failed."
case Decode = "Unable to decode the provided data."
case Duplicate = "The item already exists."
case InteractionNotAllowed = "Interaction with the Security Server is not allowed."
case NoError = "No error."
case NotAvailable = "No trust results are available."
case NotFound = "The item cannot be found."
case Param = "One or more parameters passed to the function were not valid."
case Unimplemented = "Function or operation not implemented."
}
enum LocksmithErrorCode: Int {
case RequestNotSet = 1
case TypeNotFound = 2
case UnableToClear = 3
}
enum LocksmithErrorMessage: String {
case RequestNotSet = "keychainRequest was not set."
case TypeNotFound = "The type of request given was undefined."
case UnableToClear = "Unable to clear the keychain"
}
class func keychainError(forCode statusCode: Int) -> NSError? {
var error: NSError?
if statusCode != Int(errSecSuccess) {
let message = errorMessage(statusCode)
// println("Keychain request failed. Code: \(statusCode). Message: \(message)")
error = NSError(domain: LocksmithErrorDomain, code: statusCode, userInfo: ["message": message])
}
return error
return resultsDictionary
}
// MARK: Private methods
private class func internalErrorMessage(forCode statusCode: Int) -> NSString {
switch statusCode {
case LocksmithErrorCode.RequestNotSet.rawValue:
return LocksmithErrorMessage.RequestNotSet.rawValue
case LocksmithErrorCode.UnableToClear.rawValue:
return LocksmithErrorMessage.UnableToClear.rawValue
default:
return "Error message for code \(statusCode) not set"
}
private class func performUpdate(request: CFDictionaryRef, inout result: AnyObject?) -> OSStatus {
// We perform updates to the keychain by first deleting the matching object, then writing to it with the new value.
SecItemDelete(request)
// Even if the delete request failed (e.g. if the item didn't exist before), still try to save the new item.
// If we get an error saving, we'll tell the user about it.
let status: OSStatus = withUnsafeMutablePointer(&result) { SecItemAdd(request, UnsafeMutablePointer($0)) }
return status
}
private class func parseRequest(request: LocksmithRequest) -> NSMutableDictionary {
@@ -127,9 +117,10 @@ public class Locksmith: NSObject {
options[String(kSecAttrAccessGroup)] = request.group
options[String(kSecAttrService)] = request.service
options[String(kSecAttrSynchronizable)] = request.synchronizable
options[String(kSecClass)] = securityCode(request.securityClass)
options[String(kSecClass)] = request.securityClass.rawValue
if let accessibleMode = request.accessible {
options[String(kSecAttrAccessible)] = accessible(accessibleMode)
options[String(kSecAttrAccessible)] = accessibleMode.rawValue
}
for (key, option) in options {
@@ -177,97 +168,39 @@ public class Locksmith: NSObject {
private class func parseDeleteRequest(request: LocksmithRequest, inDictionary dictionary: NSMutableDictionary) -> NSMutableDictionary {
return dictionary
}
private class func errorMessage(code: Int) -> NSString {
switch code {
case Int(errSecAllocate):
return ErrorMessage.Allocate.rawValue
case Int(errSecAuthFailed):
return ErrorMessage.AuthFailed.rawValue
case Int(errSecDecode):
return ErrorMessage.Decode.rawValue
case Int(errSecDuplicateItem):
return ErrorMessage.Duplicate.rawValue
case Int(errSecInteractionNotAllowed):
return ErrorMessage.InteractionNotAllowed.rawValue
case Int(errSecItemNotFound):
return ErrorMessage.NotFound.rawValue
case Int(errSecNotAvailable):
return ErrorMessage.NotAvailable.rawValue
case Int(errSecParam):
return ErrorMessage.Param.rawValue
case Int(errSecSuccess):
return ErrorMessage.NoError.rawValue
case Int(errSecUnimplemented):
return ErrorMessage.Unimplemented.rawValue
default:
return "Undocumented error with code \(code)."
}
}
private class func securityCode(securityClass: SecurityClass) -> CFStringRef {
switch securityClass {
case .GenericPassword:
return kSecClassGenericPassword
case .Certificate:
return kSecClassCertificate
case .Identity:
return kSecClassIdentity
case .InternetPassword:
return kSecClassInternetPassword
case .Key:
return kSecClassKey
}
}
private class func accessible(accessible: Accessible) -> CFStringRef {
switch accessible {
case .WhenUnlock:
return kSecAttrAccessibleWhenUnlocked
case .AfterFirstUnlock:
return kSecAttrAccessibleAfterFirstUnlock
case .Always:
return kSecAttrAccessibleAlways
case .WhenPasscodeSetThisDeviceOnly:
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
case .WhenUnlockedThisDeviceOnly:
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
case .AfterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
case .AlwaysThisDeviceOnly:
return kSecAttrAccessibleAlwaysThisDeviceOnly
}
}
}
// MARK: Convenient Class Methods
extension Locksmith {
public class func saveData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) -> NSError? {
public class func saveData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
let saveRequest = LocksmithRequest(userAccount: userAccount, requestType: .Create, data: data, service: service)
let (_, error) = Locksmith.performRequest(saveRequest)
return error
try Locksmith.performRequest(saveRequest)
}
public class func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> (NSDictionary?, NSError?) {
public class func loadDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> NSDictionary? {
let readRequest = LocksmithRequest(userAccount: userAccount, service: service)
return Locksmith.performRequest(readRequest)
do {
let dictionary = try Locksmith.performRequest(readRequest)
return dictionary
} catch {
return nil
}
}
public class func deleteDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) -> NSError? {
public class func deleteDataForUserAccount(userAccount: String, inService service: String = LocksmithDefaultService) throws {
let deleteRequest = LocksmithRequest(userAccount: userAccount, requestType: .Delete, service: service)
let (_, error) = Locksmith.performRequest(deleteRequest)
return error
try Locksmith.performRequest(deleteRequest)
}
public class func updateData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) -> NSError? {
public class func updateData(data: Dictionary<String, String>, forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws {
let updateRequest = LocksmithRequest(userAccount: userAccount, requestType: .Update, data: data, service: service)
let (_, error) = Locksmith.performRequest(updateRequest)
return error
try Locksmith.performRequest(updateRequest)
}
public class func clearKeychain() -> NSError? {
public class func clearKeychain() throws {
// Delete all of the keychain data of the given class
func deleteDataForSecClass(secClass: CFTypeRef) -> NSError? {
func deleteDataForSecClass(secClass: CFTypeRef) throws {
let request = NSMutableDictionary()
request.setObject(secClass, forKey: String(kSecClass))
@@ -275,45 +208,31 @@ extension Locksmith {
if let status = status {
let statusCode = Int(status)
return Locksmith.keychainError(forCode: statusCode)
if let error = LocksmithError(fromStatusCode: statusCode) {
throw error
}
}
return nil
}
// For each of the sec class types, delete all of the saved items of that type
let classes = [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity]
let errors: [NSError?] = classes.map({
return deleteDataForSecClass($0)
})
// Remove those that were successful, or failed with an acceptable error code
let filtered = errors.filter({
if let error = $0 {
for classType in classes {
do {
try deleteDataForSecClass(classType)
} catch let error as LocksmithError {
// There was an error
// If the error indicates that there was no item with that sec class, that's fine.
// If the error indicates that there was no item with that security class, that's fine.
// Some of the sec classes will have nothing in them in most cases.
return error.code != Int(errSecItemNotFound) ? true : false
if error != LocksmithError.NotFound {
throw LocksmithError.UnableToClear
}
}
// There was no error
return false
})
// If the filtered array is empty, then everything went OK
if filtered.isEmpty {
return nil
}
// At least one of the delete operations failed
let code = LocksmithErrorCode.UnableToClear.rawValue
let message = internalErrorMessage(forCode: code)
return NSError(domain: LocksmithErrorDomain, code: code, userInfo: ["message": message])
}
}
// MARK: Dictionary Extensions
// MARK: Dictionary Extension
extension NSMutableDictionary {
func setOptional(optional: AnyObject?, forKey key: NSCopying) {
if let object: AnyObject = optional {
+102 -9
View File
@@ -8,23 +8,116 @@
import UIKit
import Security
public enum SecurityClass: Int {
// With thanks to http://iosdeveloperzone.com/2014/10/22/taming-foundation-constants-into-swift-enums/
// MARK: Security Class
public enum SecurityClass: RawRepresentable {
case GenericPassword, InternetPassword, Certificate, Key, Identity
public init?(rawValue: String) {
switch rawValue {
case String(kSecClassGenericPassword):
self = GenericPassword
case String(kSecClassInternetPassword):
self = InternetPassword
case String(kSecClassCertificate):
self = Certificate
case String(kSecClassKey):
self = Key
case String(kSecClassIdentity):
self = Identity
default:
print("SecurityClass: Invalid raw value provided. Defaulting to .GenericPassword")
self = GenericPassword
}
}
public var rawValue: String {
switch self {
case .GenericPassword:
return String(kSecClassGenericPassword)
case .InternetPassword:
return String(kSecClassInternetPassword)
case .Certificate:
return String(kSecClassCertificate)
case .Key:
return String(kSecClassKey)
case .Identity:
return String(kSecClassIdentity)
}
}
}
public enum MatchLimit: Int {
// MARK: Accessible
public enum Accessible: RawRepresentable {
case WhenUnlocked, AfterFirstUnlock, Always, WhenUnlockedThisDeviceOnly, AfterFirstUnlockThisDeviceOnly, AlwaysThisDeviceOnly
@available (iOS 8,*)
case WhenPasscodeSetThisDeviceOnly
public init?(rawValue: String) {
switch rawValue {
case String(kSecAttrAccessibleWhenUnlocked):
self = WhenUnlocked
case String(kSecAttrAccessibleAfterFirstUnlock):
self = AfterFirstUnlock
case String(kSecAttrAccessibleAlways):
self = Always
case String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly):
self = WhenUnlockedThisDeviceOnly
case String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly):
self = AfterFirstUnlockThisDeviceOnly
case String(kSecAttrAccessibleAlwaysThisDeviceOnly):
self = AlwaysThisDeviceOnly
default:
if #available(iOS 8,*) {
if rawValue == String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly) {
self = WhenPasscodeSetThisDeviceOnly
} else {
print("Accessible: invalid rawValue provided. Defaulting to Accessible.WhenUnlocked.")
self = WhenUnlocked
}
} else {
print("Accessible: invalid rawValue provided. Defaulting to Accessible.WhenUnlocked.")
self = WhenUnlocked
}
}
}
public var rawValue: String {
switch self {
case .WhenUnlocked:
return String(kSecAttrAccessibleWhenUnlocked)
case .AfterFirstUnlock:
return String(kSecAttrAccessibleAfterFirstUnlock)
case .Always:
return String(kSecAttrAccessibleAlways)
case .WhenPasscodeSetThisDeviceOnly:
if #available(iOS 8.0, *) {
return String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)
} else {
fatalError("Accessible.WhenPasscodeSetThisDeviceOnly has no raw representation in iOS 7.")
}
case .WhenUnlockedThisDeviceOnly:
return String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)
case .AfterFirstUnlockThisDeviceOnly:
return String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
case .AlwaysThisDeviceOnly:
return String(kSecAttrAccessibleAlwaysThisDeviceOnly)
}
}
}
// MARK: Match Limit
public enum MatchLimit {
case One, Many
}
public enum RequestType: Int {
// MARK: Request Type
public enum RequestType {
case Create, Read, Update, Delete
}
public enum Accessible: Int {
case WhenUnlock, AfterFirstUnlock, Always, WhenPasscodeSetThisDeviceOnly,
WhenUnlockedThisDeviceOnly, AfterFirstUnlockThisDeviceOnly, AlwaysThisDeviceOnly
}
// MARK: Locksmith Request
public class LocksmithRequest: NSObject, CustomDebugStringConvertible {
// Keychain Options
// Required
@@ -42,7 +135,7 @@ public class LocksmithRequest: NSObject, CustomDebugStringConvertible {
// Debugging
override public var debugDescription: String {
return "service: \(self.service), type: \(self.type.rawValue), userAccount: \(self.userAccount)"
return "service: \(self.service), type: \(self.type), userAccount: \(self.userAccount)"
}
required public init(userAccount: String, service: String = LocksmithDefaultService) {
+10 -38
View File
@@ -1,5 +1,3 @@
> This is Locksmiths compatibility branch for Swift 1.2
# Locksmith
A sane way to work with the iOS Keychain in Swift.
@@ -16,9 +14,10 @@ A sane way to work with the iOS Keychain in Swift.
### CocoaPods
Locksmith is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
Locksmith for Swift 2, simply add the following line to your Podfile:
pod 'Locksmith', :git => 'https://github.com/matthewpalmer/Locksmith.git', :branch => '2.0'
pod "Locksmith", :git => 'https://github.com/matthewpalmer/Locksmith.git', :branch => '1.2.2'
### Manual
@@ -30,53 +29,26 @@ In the following examples, you can choose not to provide a value for the `inServ
**Save data**
- writes the data to the keychain if it does not exist already
```swift
let error = Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount")
```
**Save data, specifying a service**
```swift
let error = Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount", inService: "myService")
try Locksmith.saveData(["some key": "some value"], forUserAccount: "myUserAccount", inService: "myService")
```
**Load data**
```swift
let (dictionary, error) = Locksmith.loadDataForUserAccount("myUserAccount")
```
**Load data, specifying a service**
```swift
let (dictionary, error) = Locksmith.loadDataForUserAccount("myUserAccount", inService: "myService")
let dictionary = Locksmith.loadDataForUserAccount("myUserAccount", inService: "myService")
```
**Update data**
- overwrites whatever is stored on the keychain under this user account (if nothing is stored, we save as normal)
```swift
let error = Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount")
```
**Update data, specifying a service**
```swift
let error = Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount", inService: "myService")
try Locksmith.updateData(["some key": "another value"], forUserAccount: "myUserAccount", inService: "myService")
```
**Delete data**
```swift
let error = Locksmith.deleteDataForUserAccount("myUserAccount")
```
**Delete data, specifying a service**
```swift
let error = Locksmith.deleteDataForUserAccount("myUserAccount", inService: "myService")
try Locksmith.deleteDataForUserAccount("myUserAccount", inService: "myService")
```
## Custom Requests
@@ -88,19 +60,19 @@ To create custom keychain requests, you first have to instantiate a `LocksmithRe
let saveRequest = LocksmithRequest(userAccount: userAccount, data: ["some key": "some value"], service: service)
// Customize the request
saveRequest.synchronizable = true
Locksmith.performRequest(saveRequest)
try Locksmith.performRequest(saveRequest)
```
**Reading**
```swift
let readRequest = LocksmithRequest(userAccount: userAccount, service: service)
let (dictionary, error) = Locksmith.performRequest(readRequest)
let dictionary = try Locksmith.performRequest(readRequest)
```
**Deleting**
```swift
let deleteRequest = LocksmithRequest(userAccount: userAccount, requestType: .Delete, service: service)
Locksmith.performRequest(deleteRequest)
try Locksmith.performRequest(deleteRequest)
```
## LocksmithRequest